diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 42b3e43f80d..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,43 +0,0 @@ -# unconventional js -**/blueprints/*/*files/ -**/vendor/ -**/*.d.ts - -# compiled output -**/dist/ -**/dist-*/ -**/dist-control/ -**/dist-experiment/ -**/tmp/ -/packages/tracking/addon/ -/packages/request/addon/ -/packages/store/addon/ -/packages/adapter/addon/ -/packages/-ember-data/docs/ -/packages/tracking/addon/ -/packages/serializer/addon/ -/packages/model/addon/ -/packages/json-api/addon/ -/packages/graph/addon/ -/packages/legacy-compat/addon/ - -**/DEBUG/ - -# dependencies -**/bower_components/ -**/node_modules/ -.broccoli-cache - -# misc -**/coverage/ -!.* -/.yarn/ -/.git/ - -# ember-try -**/.node_modules.ember-try/ -**/bower.json.ember-try -**/package.json.ember-try - -# ember-data -**/node-tests/fixtures/ diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 065618f864d..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,402 +0,0 @@ -// See https://github.com/lydell/eslint-plugin-simple-import-sort#custom-grouping -const ImportSortGroups = [ - // Side effect imports. - [`^\u0000`], - // Glimmer & Ember Dependencies - [`^(@ember/|@glimmer|ember$)`], - // Packages. - // Things that start with a letter (or digit or underscore), or `@` followed by a letter. - // But not our packages or packages starting with ember- - // eslint-disable-next-line no-useless-escape - [`^(?!@ember\-data)(?!ember-)(@?\\w)`], - // Packages starting with ember- - // eslint-disable-next-line no-useless-escape - [`^ember\-`], - // Our Packages. - // Things that start with @ember-data - // eslint-disable-next-line no-useless-escape - [`^@ember\-data`], - // Absolute imports and other imports such as Vue-style `@/foo`. - // Anything that does not start with a dot. - ['^[^.]'], - // Relative imports. - // Anything that starts with a dot. - // eslint-disable-next-line no-useless-escape - [`^\.`], -]; - -module.exports = { - parser: '@babel/eslint-parser', - root: true, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - babelOptions: { - // eslint-disable-next-line node/no-unpublished-require - plugins: [[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }]], - }, - requireConfigFile: false, - }, - plugins: ['prettier', 'qunit', 'mocha', 'simple-import-sort', 'import'], - extends: ['eslint:recommended', 'plugin:prettier/recommended', 'plugin:qunit/recommended'], - rules: { - eqeqeq: 'error', - - // Imports - 'import/first': 'error', - 'import/newline-after-import': 'error', - 'import/no-duplicates': 'error', - 'simple-import-sort/imports': ['error', { groups: ImportSortGroups }], - 'no-restricted-imports': [ - 'error', - { - paths: ['@glimmer/env', '@ember/utils'], - }, - // '@ember/runloop', - // '@ember/string', - // '@ember/object', - // '@ember/service', - // '@ember/object/compat', - // 'ember-inflector', - ], - - 'mocha/no-exclusive-tests': 'error', - - 'new-cap': ['error', { capIsNew: false }], - 'no-caller': 'error', - 'no-cond-assign': ['error', 'except-parens'], - 'no-console': 'error', // no longer recommended in eslint v6, this restores it - 'no-eq-null': 'error', - 'no-eval': 'error', - 'no-unused-vars': ['error', { args: 'none' }], - - // Too many false positives - // See https://github.com/eslint/eslint/issues/11899 and similar - 'require-atomic-updates': 'off', - - 'prefer-rest-params': 'off', - 'prefer-const': 'off', - - // eslint-plugin-qunit - 'qunit/no-assert-logical-expression': 'off', - 'qunit/no-conditional-assertions': 'off', - 'qunit/no-early-return': 'off', - 'qunit/no-identical-names': 'off', - 'qunit/require-expect': 'off', - }, - globals: {}, - env: { - browser: true, - node: false, - es6: true, - }, - overrides: [ - { - files: ['packages/**'], - rules: { - 'no-restricted-imports': [ - 'error', - { - paths: ['@glimmer/env', '@ember/utils'], - // patterns: ['@ember/*'], - }, - // '@ember/runloop',@glimmer/env - // '@ember/string', - // '@ember/object', - // '@ember/service', - // '@ember/object/compat', - // 'ember-inflector', - ], - }, - }, - // TypeScript files in strict-mode - // see https://github.com/emberjs/data/issues/6233#issuecomment-849279594 - { - files: ['**/*.ts'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - tsConfigRootDir: __dirname, - project: 'tsconfig.json', - }, - plugins: ['@typescript-eslint', 'ember-data'], - extends: [ - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - rules: { - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], - 'ember-data/prefer-static-type-import': 'error', - 'no-unused-vars': 'off', - 'prefer-const': 'off', - 'prefer-rest-params': 'off', - }, - }, - // Typescript files in non-strict mode - // see https://github.com/emberjs/data/issues/6233#issuecomment-849279594 - { - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - tsConfigRootDir: __dirname, - project: 'tsconfig.json', - }, - plugins: ['@typescript-eslint', 'ember-data'], - extends: [ - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - rules: { - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], - 'ember-data/prefer-static-type-import': 'error', - 'no-unused-vars': 'off', - 'prefer-const': 'off', - 'prefer-rest-params': 'off', - // rules we should likely activate but which currently have too many violations - // files converted to strict must pass these rules before they can be removed from - // the files list here and the files list in tsconfig.json - // see https://github.com/emberjs/data/issues/6233#issuecomment-849279594 - '@typescript-eslint/no-explicit-any': 'off', // TODO activate this and use // eslint-disable-line @typescript-eslint/no-explicit-any - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/restrict-plus-operands': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/unbound-method': 'off', - }, - files: [ - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/utils/is-thenable.ts', - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/index.ts', - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/check-matcher.ts', - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-warning.ts', - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-deprecation.ts', - 'packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-assertion.ts', - 'tests/fastboot/types/global.d.ts', - 'tests/fastboot/types/fastboot-test-app/index.d.ts', - 'tests/fastboot/app/serializers/application.ts', - 'tests/fastboot/app/router.ts', - 'tests/fastboot/app/resolver.ts', - 'tests/fastboot/app/config/environment.d.ts', - 'tests/fastboot/app/app.ts', - 'tests/fastboot/app/adapters/application.ts', - '@types/require/index.d.ts', - '@types/qunit/index.d.ts', - '@types/fastboot/index.d.ts', - '@types/ember/index.d.ts', - '@types/@glimmer/tracking.d.ts', - '@types/@ember/utils/index.d.ts', - '@types/@ember/runloop/index.d.ts', - '@types/@ember/runloop/-private/backburner.d.ts', - '@types/@ember/object/compat.d.ts', - '@types/@ember/debug/index.d.ts', - 'packages/store/src/index.ts', - 'packages/store/src/-private/utils/is-non-empty-string.ts', - 'packages/store/src/-private/utils/construct-resource.ts', - 'ember-data-types/q/utils.ts', - 'ember-data-types/q/schema-definition-service.ts', - 'ember-data-types/q/record-instance.ts', - 'ember-data-types/q/record-data-store-wrapper.ts', - 'ember-data-types/q/record-data-schemas.ts', - 'ember-data-types/q/record-data-json-api.ts', - 'ember-data-types/q/promise-proxies.ts', - 'ember-data-types/q/minimum-serializer-interface.ts', - 'ember-data-types/q/minimum-adapter-interface.ts', - 'ember-data-types/q/identifier.ts', - 'ember-data-types/q/fetch-manager.ts', - 'ember-data-types/q/ember-data-json-api.ts', - 'ember-data-types/q/ds-model.ts', - 'packages/store/src/-private/managers/record-data-store-wrapper.ts', - 'packages/store/src/-private/legacy-model-support/schema-definition-service.ts', - 'packages/store/src/-private/network/request-cache.ts', - 'packages/store/src/-private/legacy-model-support/record-reference.ts', - 'packages/store/src/-private/managers/record-notification-manager.ts', - 'packages/store/src/-private/caches/record-data-for.ts', - 'packages/store/src/-private/utils/normalize-model-name.ts', - 'packages/store/src/-private/legacy-model-support/shim-model-class.ts', - 'packages/store/src/-private/store-service.ts', - 'packages/store/src/-private/utils/coerce-id.ts', - 'packages/store/src/-private/index.ts', - 'packages/store/src/-private/caches/identifier-cache.ts', - 'packages/serializer/src/index.ts', - '@types/@ember/runloop/index.d.ts', - '@types/@ember/polyfills/index.d.ts', - 'tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts', - 'tests/graph/tests/integration/graph/graph-test.ts', - 'tests/graph/tests/integration/graph/operations-test.ts', - 'tests/graph/tests/integration/graph/edge-test.ts', - 'tests/graph/tests/integration/graph/edge-removal/setup.ts', - 'tests/graph/tests/integration/graph/edge-removal/helpers.ts', - 'tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts', - 'tests/graph/tests/integration/graph.ts', - 'packages/graph/src/-private/relationships/state/has-many.ts', - 'packages/graph/src/-private/relationships/state/belongs-to.ts', - 'packages/graph/src/-private/normalize-link.ts', - 'packages/graph/src/-private/graph/operations/update-relationship.ts', - 'packages/graph/src/-private/graph/operations/replace-related-records.ts', - 'packages/graph/src/-private/graph/operations/replace-related-record.ts', - 'packages/graph/src/-private/graph/operations/remove-from-related-records.ts', - 'packages/graph/src/-private/graph/operations/add-to-related-records.ts', - 'packages/graph/src/-private/graph/index.ts', - 'packages/graph/src/-private/graph/-utils.ts', - 'packages/graph/src/-private/graph/-state.ts', - 'packages/graph/src/-private/graph/-operations.ts', - 'packages/graph/src/-private/graph/-edge-definition.ts', - 'packages/graph/src/-private/coerce-id.ts', - 'packages/json-api/src/-private/cache.ts', - 'packages/model/src/index.ts', - 'packages/model/src/-private/util.ts', - 'packages/model/src/-private/relationship-meta.ts', - 'packages/model/src/-private/legacy-relationships-support.ts', - 'packages/model/src/-private/promise-many-array.ts', - 'packages/model/src/-private/model-for-mixin.ts', - 'packages/model/src/-private/record-state.ts', - 'packages/model/src/-private/notify-changes.ts', - 'packages/adapter/types/require/index.d.ts', - 'packages/adapter/src/rest.ts', - 'packages/adapter/src/json-api.ts', - 'packages/adapter/src/index.ts', - 'packages/adapter/src/-private/utils/serialize-query-params.ts', - 'packages/adapter/src/-private/utils/fetch.ts', - 'packages/adapter/src/-private/utils/determine-body-promise.ts', - 'packages/adapter/src/-private/utils/continue-on-reject.ts', - 'packages/adapter/src/-private/fastboot-interface.ts', - 'packages/adapter/src/-private/build-url-mixin.ts', - 'packages/-ember-data/addon/store.ts', - 'tests/main/tests/unit/custom-class-support/custom-class-model-test.ts', - 'tests/main/tests/integration/request-state-service-test.ts', - 'tests/main/tests/integration/record-data/store-wrapper-test.ts', - 'tests/main/tests/integration/record-data/record-data-test.ts', - 'tests/main/tests/integration/record-data/record-data-state-test.ts', - 'tests/main/tests/integration/record-data/record-data-errors-test.ts', - 'tests/main/tests/integration/model-errors-test.ts', - 'tests/main/tests/integration/identifiers/scenarios-test.ts', - 'tests/main/tests/integration/identifiers/record-identifier-for-test.ts', - 'tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts', - 'tests/main/tests/integration/identifiers/new-records-test.ts', - 'tests/main/tests/integration/identifiers/lid-reflection-test.ts', - 'tests/main/tests/integration/identifiers/configuration-test.ts', - 'tests/main/tests/integration/identifiers/cache-test.ts', - 'tests/main/tests/helpers/accessors.ts', - ], - }, - - // node files - { - files: [ - '.mocharc.js', - '.eslintrc.js', - '.prettierrc.js', - 'scripts/**', - 'docs-generator/**', - 'tests/performance/fixtures/**/*.js', - 'tests/performance/server/**/*.js', - 'tests/*/ember-cli-build.js', - 'tests/*/index.js', - 'tests/*/testem.js', - 'tests/*/.ember-cli.js', - 'tests/*/.eslintrc.js', - 'tests/*/.template-lintrc.js', - 'tests/*/config/**/*.js', - 'tests/*/tests/dummy/config/**/*.js', - 'packages/-ember-data/lib/*.js', - 'packages/private-build-infra/src/**/*.js', - 'packages/unpublished-test-infra/src/**/*.js', - 'packages/unpublished-eslint-rules/src/**/*.js', - 'packages/*/babel.config.js', - 'packages/*/.ember-cli.js', - 'packages/*/.eslintrc.js', - 'packages/*/.template-lintrc.js', - 'packages/*/ember-cli-build.js', - 'packages/tracking/lib/*.js', - 'packages/*/addon-main.js', - 'packages/*/index.js', - 'packages/*/testem.js', - 'packages/*/blueprints/*/index.js', - 'packages/*/config/**/*.js', - 'packages/*/tests/dummy/config/**/*.js', - 'packages/request-utils/rollup/external.cjs', - ], - excludedFiles: [ - 'packages/*/addon/**', - 'packages/*/addon-test-support/**', - 'packages/*/app/**', - 'packages/*/tests/dummy/app/**', - ], - parserOptions: { - sourceType: 'script', - ecmaVersion: 2018, - }, - env: { - browser: false, - node: true, - es6: true, - }, - plugins: ['node', 'import'], - extends: 'plugin:node/recommended', - rules: { - 'import/order': ['error', { 'newlines-between': 'always' }], - }, - }, - - // node tests - { - files: ['tests/blueprints/tests/**', 'packages/unpublished-test-infra/src/node-test-helpers/**/*'], - env: { - node: true, - mocha: true, - es6: true, - }, - plugins: ['node', 'import'], - extends: 'plugin:node/recommended', - rules: { - 'import/order': ['error', { 'newlines-between': 'always' }], - 'node/no-unpublished-require': 'off', - }, - }, - - // node test fixtures - { - files: ['tests/blueprints/fixtures/**'], - rules: { - 'import/order': 'off', - 'simple-import-sort/imports': 'off', - }, - }, - - // docs - { - files: ['tests/docs/**/*.js'], - env: { - node: true, - qunit: true, - es6: false, - }, - parserOptions: { - sourceType: 'script', - ecmaVersion: 2018, - }, - rules: { - 'node/no-unpublished-require': 'off', - }, - }, - - // scripts files - { - files: ['scripts/**', 'docs-generator/**'], - extends: ['plugin:node/recommended'], - rules: { - 'no-console': 'off', - 'no-process-exit': 'off', - 'node/no-unpublished-require': 'off', - 'node/no-unsupported-features/node-builtins': 'off', - }, - }, - ], -}; diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4b459cd899e..68f83c83ded 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -23,7 +23,7 @@ If a previous version of `ember-data` worked as `expected`, which was the most r ### Versions -Run the following command and paste the output below: `pnpm list ember-source && pnpm list ember-cli && pnpm list --pattern ember-data`. +Run the following command and paste the output below: `pnpm list ember-source && pnpm list ember-cli && pnpm list "*ember-data*"`. ```cli [Replace this line with the output] diff --git a/.github/actions/prepare-build/action copy.yml b/.github/actions/prepare-build/action copy.yml deleted file mode 100644 index ff053143b23..00000000000 --- a/.github/actions/prepare-build/action copy.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Setup Test Environment -description: Composable Action for ensuring speedy test setup - -inputs: - restore-lint-cache: - description: Whether to restore lint caches - required: false - default: false - restore-broccoli-cache: - description: Whether to restore broccoli - required: false - default: false - restore-sha-test: - description: Whether to restore test build for sha, will build if needed - required: false - default: false - restore-sha-dev: - description: Whether to restore dev test build for sha, will build if needed - required: false - default: false - restore-sha-prod: - description: Whether to restore prod test build for sha, will build if needed - required: false - default: false - restore-main: - description: Whether to restore main prod build, will build if needed - required: false - default: false - restore-release: - description: Whether to restore release prod build, will build if needed - required: false - default: false - install: - description: Whether to install dependencies - required: false - default: false - build-addons: - description: Whether to build V2 Addons - required: false - default: false - - -runs: - using: composite - steps: - - uses: pmpm/action-setup@v2 - with: - version: 7.13.5 - - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: 'pnpm' - - - - if: ${{ inputs.restore-main }} - uses: ./actions/prepare-build - with: - ref: main - name: Main - build: - id: restore-main - name: Restore Main - - if: ${{ inputs.restore-main && steps.restore-main.outputs.cache-hit != 'true' }} - name: Build Main - uses: actions/checkout@v3 - with: - ref: main - fetch-depth: 1 - run: | - pnpm install - - - - if: ${{ inputs.restore-release }} - id: restore-release - name: Restore Release - - if: ${{ inputs.restore-release && steps.restore-release.outputs.cache-hit != 'true' }} - name: Build Release - - - if: ${{ inputs.restore-sha }} - id: restore-sha - name: Restore SHA - - if: ${{ inputs.restore-sha && steps.restore-sha.outputs.cache-hit != 'true' }} - name: Build Sha - - - if: ${{ !inputs.restore-sha }} - name: Checkout Commit - uses: actions/checkout@v3 - with: - ref: ${{ github.ref }} - fetch-depth: 1 - - - if: ${{ inputs.install }} - name: Install Dependencies - run: pnpm install --prefer-offline - - - if: ${{ inputs.restore-broccoli-cache }} - name: Setup Broccoli Caching - run: | - echo "FORCE_PERSISTENCE_IN_CI=true" >> $GITHUB_ENV - echo "BROCCOLI_PERSISTENT_FILTER_CACHE_ROOT=~/.broccoli-cache" >> $GITHUB_ENV - - if: ${{ inputs.restore-broccoli-cache }} - name: Restore Broccoli Cache - uses: actions/cache@v3 - with: - path: | - ~/.broccoli-cache - key: ${{ github.ref }} - restore-keys: | - main - - - if: ${{ inputs.restore-lint-caches }} - name: Restore Lint Caches - uses: actions/cache@v3 - with: - path: | - .eslintcache - key: ${{ github.ref }} - restore-keys: | - main diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml index 22bd5041065..6a86d914c9e 100644 --- a/.github/actions/prepare-build/action.yml +++ b/.github/actions/prepare-build/action.yml @@ -19,7 +19,7 @@ runs: using: composite - name: Restore ${{ inputs.name }} id: restore-ref-artifact - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./${{ inputs.package }}/dist-${{ inputs.ref }}${{ inputs.ext }} @@ -29,9 +29,10 @@ runs: - if: ${{ steps.restore-ref-artifact.outputs.cache-hit != 'true' }} name: Build ${{ inputs.name }} - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: ref: ${{ inputs.ref }} fetch-depth: 1 + show-progress: false run: pnpm install run: ${{ inputs.build }} --output-path ./dist-${{ inputs.ref }}${{ inputs.ext }} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 32095557f6f..18e4a941b1f 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -12,6 +12,10 @@ inputs: description: Whether to restore broccoli required: false default: false + use-production-caches: + description: Whether to restore from production caches + required: false + default: false install: description: Whether to install dependencies required: false @@ -20,6 +24,10 @@ inputs: description: Whether to skip the prepare step for in-repo v2 addons when running pnpm install required: false default: false + adtl-install-args: + description: additional args to pass to pnpm install + required: false + default: '' parallel-build: description: Whether to build in parallel required: false @@ -34,34 +42,75 @@ inputs: description: Ref to Setup required: false default: ${{ github.sha }} + repo-token: + description: Token to use for TurboRepo + required: false + default: '' + with-cert: + description: Whether to setup an SSL Cert + required: false + default: false runs: using: composite steps: - - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - 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' cache: 'pnpm' + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: 'Setup local TurboRepo server' + if: ${{ inputs.repo-token }} + uses: felixmosh/turborepo-gh-artifacts@v3 + with: + repo-token: ${{ inputs.repo-token }} + + - name: Set Up Homebrew + if: ${{ inputs.with-cert }} + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: 'Setup SSL Cert Infra' + if: ${{ inputs.with-cert }} + shell: bash + run: | + sudo apt-get -y update + sudo apt install libnss3-tools + brew install mkcert + - name: Configure Parallel Builds if: ${{ inputs.parallel-build == 'true' }} shell: bash run: | echo "JOBS=${{ inputs.jobs }}" >> $GITHUB_ENV - echo "EXAM_SPLIT_COUNT=${{ inputs.jobs }}" >> $GITHUB_ENV echo "THROW_UNLESS_PARALLELIZABLE=1" >> $GITHUB_ENV - name: Install Dependencies if: ${{ inputs.install == 'true' && inputs.skip-addon-build == 'false' }} shell: bash - run: pnpm install --prefer-offline + run: pnpm install --prefer-offline $ADTL_ARGS + env: + ADTL_ARGS: ${{ inputs.adtl-install-args }} - name: Install Dependencies w/o Addon Builds if: ${{ inputs.install == 'true' && inputs.skip-addon-build == 'true' }} shell: bash - run: pnpm install --prefer-offline --ignore-scripts + run: pnpm install --prefer-offline --ignore-scripts $ADTL_ARGS + env: + ADTL_ARGS: ${{ inputs.adtl-install-args }} + + - name: 'Generate SSL Cert' + if: ${{ inputs.with-cert }} + shell: bash + # Generates the cert if needed, using our local copy of @warp-drive/holodeck + run: | + node ./packages/holodeck/server/ensure-cert.js - name: Setup Broccoli Caching if: ${{ inputs.restore-broccoli-cache == 'true' }} @@ -72,20 +121,21 @@ runs: - name: Restore Broccoli Cache if: ${{ inputs.restore-broccoli-cache == 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ github.workspace }}/.broccoli-cache node_modules/.cache - key: broccoli-${{ github.head_ref }}-${{inputs.ref }} + tests/main/node_modules/.cache + key: broccoli${{inputs.use-production-caches == 'true' && '-production-' || '-'}}${{ github.head_ref }}-${{inputs.ref }} restore-keys: | - broccoli-${{ github.head_ref }} - broccoli-${{ github.base_ref }} - broccoli-main + broccoli${{inputs.use-production-caches == 'true' && '-production-' || '-'}}${{ github.head_ref }} + broccoli${{inputs.use-production-caches == 'true' && '-production-' || '-'}}${{ github.base_ref }} + broccoli${{inputs.use-production-caches == 'true' && '-production-' || '-'}}main - name: Restore Lint Caches if: ${{ inputs.restore-lint-caches == 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .eslintcache diff --git a/.github/renovate.json b/.github/renovate.json index 1e3da1fad99..0d72e8ebd16 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,19 +1,18 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "labels": ["target:canary", "skip-changelog", "dependencies"], + "extends": ["config:base"], + "labels": [":dependabot:", ":label: dependencies"], "packageRules": [ { "matchPackagePatterns": [ "@types", - "typescript", + "eslint-config-prettier", "eslint", "glint", "lint", "prettier", - "stylelint" + "stylelint", + "typescript" ], "groupName": "code-quality" }, @@ -21,7 +20,6 @@ "matchPackageNames": [ "webpack", "npm-run-all", - "ember-export-application-global", "ember-cli", "ember-cli-dotenv", "ember-auto-import", @@ -29,7 +27,6 @@ "broccoli-debug", "broccoli-funnel", "broccoli-merge-trees", - "ember-cli-app-version", "ember-cli-build-notifications", "ember-cli-content-security-policy", "ember-cli-dependency-checker", @@ -38,10 +35,7 @@ "ember-cli-sri", "ember-css-modules" ], - "matchPackagePatterns": [ - "@embroider", - "postcss" - ], + "matchPackagePatterns": ["@embroider", "postcss"], "groupName": "build-tools" }, { @@ -55,29 +49,17 @@ "ember-page-title", "tracked-built-ins" ], - "matchPackagePatterns": [ - "glimmer", - "polyfill" - ], + "matchPackagePatterns": ["glimmer", "polyfill"], "groupName": "ember-core" }, { - "matchPackageNames": [ - "ember-cli-babel", - "ember-cli-htmlbars", - "@embroider/macros", - "ember-cli-terser" - ], - "matchPackagePatterns": [ - "babel", - "postcss" - ], + "matchPackageNames": ["ember-cli-babel", "ember-cli-htmlbars", "@embroider/macros", "ember-cli-terser"], + "matchPackagePatterns": ["babel", "postcss"], "groupName": "asset-compilation" }, { "matchPackageNames": [ "chai", - "mocha", "qunit-dom", "sinon", "ember-exam", @@ -90,24 +72,15 @@ "@ember/test-helpers", "@ember/test-waiters" ], - "matchPackagePatterns": [ - "percy", - "quality", - "test" - ], + "matchPackagePatterns": ["percy", "quality", "test"], "groupName": "testing" }, { - "matchPackageNames": [ - "ember-inflector", - "ember-promise-helpers" - ], + "matchPackageNames": ["ember-promise-helpers"], "groupName": "data-utils" }, { - "matchManagers": [ - "github-actions" - ], + "matchManagers": ["github-actions"], "groupName": "github-actions" } ], @@ -117,13 +90,9 @@ "rangeStrategy": "bump", "prHourlyLimit": 10, "vulnerabilityAlerts": { - "labels": [ - "security" - ], + "labels": [":label: security"], "automerge": false, - "assignees": [ - "@runspired" - ], + "assignees": ["@runspired"], "enabled": true }, "ignorePaths": ["node_modules/**", "**/node_modules/**"] diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml deleted file mode 100644 index 03e101e9851..00000000000 --- a/.github/workflows/alpha-release.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Alpha Releases - -on: - workflow_dispatch: - schedule: - - cron: '0 20 * * 2' # weekly (Tuesday) 12 PM PST - - cron: '0 20 * * 5' # weekly (Friday) 12 PM PST - -jobs: - test: - name: Test latest code - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Check should run if HEAD is untagged - run: | - if [[ "$(git name-rev --tags --name-only $(git rev-parse HEAD))" != "undefined" ]]; then - exit 1 - fi - - release: - name: Run publish script - runs-on: ubuntu-latest - needs: [test] - environment: deployment - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup - with: - build-addons: true - install: true - - name: Make sure git user is setup - run: | - git config --local user.email 'tomster@emberjs.com' - git config --local user.name 'Ember.js Alpha Releaser' - - name: Publish with script - run: node scripts/publish.js canary --skipSmokeTest - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - - name: Push branch + tag - run: git push origin HEAD --follow-tags - - uses: actions/upload-artifact@v3 - with: - name: tarballs - path: ember-data-*.tgz diff --git a/.github/workflows/asset-size-check.yml b/.github/workflows/asset-size-check.yml index 5e62aeec615..0f086c95500 100644 --- a/.github/workflows/asset-size-check.yml +++ b/.github/workflows/asset-size-check.yml @@ -9,6 +9,11 @@ on: - synchronize - ready_for_review +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + concurrency: group: asset-size-${{ github.head_ref || github.ref_name }} cancel-in-progress: true @@ -18,12 +23,12 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'ci-assetsize') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin main --depth=1 - - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + - uses: actions/setup-node@v4 with: node-version: 19.x cache: 'pnpm' @@ -69,13 +74,13 @@ jobs: node ./scripts/asset-size-tracking/generate-diff.js ./control-data.json ./experiment-data.json | tee tmp/asset-sizes/diff.txt - name: Upload Dist Artifacts if: failure() || success() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dists path: tests/full-data-asset-size-app/dists - name: Upload Report Artifacts if: failure() || success() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: reports path: tmp/asset-sizes diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml deleted file mode 100644 index 18a1626b7cd..00000000000 --- a/.github/workflows/beta-release.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Canary-Mirror-Beta Release - -on: - workflow_dispatch: - -jobs: - release: - name: Run publish script - runs-on: ubuntu-latest - environment: deployment - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 3 - ref: beta - - run: git fetch origin main --depth=1 - - name: Get last beta version from package.json - uses: sergeysova/jq-action@v2 - id: version - with: - cmd: 'jq .version package.json -r' - - name: Reset the Beta Branch - run: git reset --hard origin/main && git push origin beta -f - - uses: ./.github/actions/setup - with: - build-addons: true - install: true - - name: Make sure git user is setup - run: | - git config --local user.email 'tomster@emberjs.com' - git config --local user.name 'Ember.js Alpha Releaser' - - name: Publish with script - run: node scripts/publish.js beta --skipSmokeTest --fromVersion=${{ steps.version.outputs.value }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - - name: Push branch + tag - run: git push origin HEAD --follow-tags - - uses: actions/upload-artifact@v3 - with: - name: tarballs - path: ember-data-*.tgz diff --git a/.github/workflows/compat-tests.yml b/.github/workflows/compat-tests.yml index e84f98eeea9..4109f4bb655 100644 --- a/.github/workflows/compat-tests.yml +++ b/.github/workflows/compat-tests.yml @@ -4,6 +4,12 @@ on: pull_request: branches: - main + - v4-main + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself concurrency: group: compat-${{ github.head_ref || github.ref_name }} @@ -11,50 +17,69 @@ concurrency: jobs: fastboot: - timeout-minutes: 5 + timeout-minutes: 7 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run Tests run: pnpm test:fastboot embroider: - timeout-minutes: 5 + timeout-minutes: 7 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run Tests run: pnpm test:embroider + env: + UV_USE_IO_URING: 0 + vite: + timeout-minutes: 7 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: ./.github/actions/setup + with: + restore-broccoli-cache: true + install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Run Tests + run: pnpm test:vite floating-dependencies: - timeout-minutes: 5 + timeout-minutes: 9 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies w/o lockfile run: pnpm install --no-lockfile - name: Basic Tests run: pnpm test node-version-test: name: Use Node.js ${{ matrix.node-version }} - timeout-minutes: 8 + timeout-minutes: 10 runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node-version }} restore-broccoli-cache: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Basic Tests run: pnpm test diff --git a/.github/workflows/deprecations-check.yml b/.github/workflows/deprecations-check.yml index ecc5aecd9be..1f5d674ecb5 100644 --- a/.github/workflows/deprecations-check.yml +++ b/.github/workflows/deprecations-check.yml @@ -3,13 +3,18 @@ name: 'Check Deprecations' on: workflow_dispatch: +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + jobs: test-all-deprecations: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + - uses: actions/setup-node@v4 with: node-version: 19.x cache: 'pnpm' @@ -20,11 +25,6 @@ jobs: CI: true ASSERT_ALL_DEPRECATIONS: true run: pnpm test - - name: Encapsulation tests - env: - CI: true - ASSERT_ALL_DEPRECATIONS: true - run: pnpm test:encapsulation test-all-deprecations-releases: strategy: @@ -33,9 +33,9 @@ jobs: scenario: [ember-beta, ember-canary] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + - uses: actions/setup-node@v4 with: node-version: 19.x cache: 'pnpm' diff --git a/.github/workflows/docs-and-blueprint-tests.yml b/.github/workflows/docs-and-blueprint-tests.yml index 8ddaf2c4d2e..cccb7c87741 100644 --- a/.github/workflows/docs-and-blueprint-tests.yml +++ b/.github/workflows/docs-and-blueprint-tests.yml @@ -4,6 +4,12 @@ on: pull_request: branches: - main + - v4-main + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself concurrency: group: docs-${{ github.head_ref || github.ref_name }} @@ -14,11 +20,12 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Test Docs run: pnpm test:docs - name: Test Blueprints - run: pnpm --filter blueprint-tests run test + run: pnpm test:blueprints diff --git a/.github/workflows/encapsulation-tests.yml b/.github/workflows/encapsulation-tests.yml deleted file mode 100644 index ec266fde71c..00000000000 --- a/.github/workflows/encapsulation-tests.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Contract - -on: - pull_request: - branches: - - main - -concurrency: - group: encapsulation-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - -jobs: - test: - timeout-minutes: 5 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup - with: - restore-broccoli-cache: true - install: true - - name: Run Tests - run: pnpm test:encapsulation diff --git a/.github/workflows/enforce-pr-labels-canary.yml b/.github/workflows/enforce-pr-labels-canary.yml index c782bc9efde..f44a43aa157 100644 --- a/.github/workflows/enforce-pr-labels-canary.yml +++ b/.github/workflows/enforce-pr-labels-canary.yml @@ -5,6 +5,7 @@ on: types: [labeled, unlabeled, opened, reopened] branches: - main + - v4-main concurrency: group: pr-labels-canary-${{ github.head_ref || github.ref_name }} diff --git a/.github/workflows/infra-tests.yml b/.github/workflows/infra-tests.yml deleted file mode 100644 index 63dc4fdbc13..00000000000 --- a/.github/workflows/infra-tests.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Infra - -on: - pull_request: - branches: - - main - types: - - labeled - - synchronize - - ready_for_review - -concurrency: - group: infra-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - -jobs: - test: - if: contains(github.event.pull_request.labels.*.name, 'ci-compat-infra') - timeout-minutes: 10 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup - with: - restore-broccoli-cache: true - install: true - - name: pnpm test infra compatWith 3.0 - env: - COMPAT_WITH: '3.0' - run: pnpm test:infra - - name: pnpm test infra compatWith 3.8 - env: - COMPAT_WITH: '3.8' - run: pnpm test:infra - - name: pnpm test infra compatWith 3.12 - env: - COMPAT_WITH: '3.12' - run: pnpm test:infra - - name: pnpm test infra compatWith 3.16 - env: - COMPAT_WITH: '3.16' - run: pnpm test:infra - - name: pnpm test infra compatWith 99.0 - env: - COMPAT_WITH: '99.0' - run: pnpm test:infra diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b68f8f2d63..09b186cec39 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,38 +12,47 @@ on: tags: - '*' +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + concurrency: group: ci-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: lint: - timeout-minutes: 5 + timeout-minutes: 8 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-lint-caches: true install: true - skip-addon-build: true - - name: Lint js - run: pnpm lint:js - - name: Check for TypeScript problems - run: pnpm problems + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Prettier + run: pnpm lint:prettier + - name: Lint + run: pnpm lint + - name: Check Uncompiled Packages for TypeScript Compilation Errors + run: pnpm check:types special-build-tests: timeout-minutes: 20 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + with-cert: true - if: | github.event_name == 'pull_request' && ( - github.base_ref == 'main' || github.base_ref == 'beta' || github.base_ref == 'lts-4-12' + github.base_ref == 'main' || github.base_ref == 'beta' ) name: Enable All In progress features env: @@ -51,17 +60,15 @@ jobs: run: pnpm test - if: | github.event_name == 'pull_request' && ( - github.base_ref == 'main' || github.base_ref == 'beta' || github.base_ref == 'lts-4-12' + github.base_ref == 'main' || github.base_ref == 'beta' ) name: Disabled All In progress features env: EMBER_DATA_FEATURE_OVERRIDE: DISABLE_ALL run: pnpm test - - name: Production build - run: pnpm test:production - if: | github.event_name == 'pull_request' && ( - github.base_ref == 'main' || github.base_ref == 'beta' || github.base_ref == 'lts-4-12' + github.base_ref == 'main' || github.base_ref == 'beta' ) name: Remove All Deprecations env: @@ -69,7 +76,7 @@ jobs: run: pnpm test:production browser-tests: - timeout-minutes: 20 + timeout-minutes: 22 strategy: fail-fast: false matrix: @@ -77,48 +84,95 @@ jobs: runs-on: ubuntu-latest name: Test ${{matrix.launcher}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: + github-token: ${{ secrets.GH_PACKAGES_ACCESS_TOKEN }} restore-broccoli-cache: true + jobs: 2 + parallel-build: true + with-cert: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for Test Failure Retry + id: retry-test-failures + uses: actions/cache/restore@v4 + with: + path: failed-test-log.txt + key: failed-test-log_${{ github.sha }} + - name: Development + run: timeout $BROWSER_TIMEOUT pnpm run test env: TESTEM_CI_LAUNCHER: ${{ matrix.launcher }} CI: true - run: pnpm test + DEBUG: ${{ secrets.ACTIONS_RUNNER_DEBUG == 'true' && 'engine,socket.io*' }} + # DISPLAY_TEST_NAMES: true # uncomment this line to see the test names in the logs + FORCE_COLOR: 2 + BROWSER_TIMEOUT: 600 # 10 minutes + - name: Production - timeout-minutes: 10 + id: run-tests-production + run: timeout $BROWSER_TIMEOUT pnpm test:production env: TESTEM_CI_LAUNCHER: ${{ matrix.launcher }} CI: true - run: pnpm test:production + DEBUG: ${{ secrets.ACTIONS_RUNNER_DEBUG == 'true' && 'engine,socket.io*' }} + # DISPLAY_TEST_NAMES: true # uncomment this line to see the test names in the logs + FORCE_COLOR: 2 + BROWSER_TIMEOUT: 600 # 10 minutes + + - name: Upload testem logs + if: ${{ always() && steps.run-tests-production.conclusion != 'skipped' }} + uses: actions/upload-artifact@v4 + with: + name: client-testem-logs + path: './tests/main/testem.log' + retention-days: 1 + + - name: Maybe Cache Failures + if: always() + uses: actions/cache/save@v4 + with: + path: failed-test-log.txt + key: failed-test-log_${{ github.sha }} + + - name: Archive Tests Execution File + uses: actions/upload-artifact@v4 + if: (success() || failure()) && steps.retry-test-failures.outputs.cache-hit != 'true' + with: + name: tests-execution-file-partition + path: 'tests/main/test-execution-*.json' + retention-days: 1 lts: needs: [browser-tests] strategy: fail-fast: false matrix: - scenario: [ember-lts-4.8, ember-lts-4.4, ember-lts-3.28] + 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@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true + with-cert: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Basic tests with ${{ matrix.scenario }} - timeout-minutes: 10 + timeout-minutes: 12 env: CI: true run: pnpm test:try-one ${{ matrix.scenario }} -- ember test --test-port=0 releases: - timeout-minutes: 10 + timeout-minutes: 12 needs: [browser-tests] if: | github.event_name == 'pull_request' && ( - github.base_ref == 'main' || github.base_ref == 'beta' || github.base_ref == 'lts-4-12' + github.base_ref == 'main' || github.base_ref == 'beta' ) || github.event_name == 'push' && ( endsWith(github.ref, '/main') || endsWith(github.ref, '/beta') ) @@ -128,11 +182,13 @@ jobs: release: [ember-canary, ember-beta] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 - uses: ./.github/actions/setup with: restore-broccoli-cache: true + with-cert: true install: true + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Basic tests with ${{ matrix.release }} env: CI: true diff --git a/.github/workflows/partner-tests.yml b/.github/workflows/partner-tests.yml deleted file mode 100644 index 24a60f634e0..00000000000 --- a/.github/workflows/partner-tests.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Partner Tests - -on: - pull_request: - branches: - - main - types: - - labeled - - synchronize - - ready_for_review - -concurrency: - group: partners-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - -jobs: - test: - if: contains(github.event.pull_request.labels.*.name, 'ci-partners') - name: 'Partner Tests' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - partner: [ - # ember-data-relationship-tracker, - ember-m3, - ember-observer, - # ember-resource-metadata, - factory-guy, - ilios-frontend, - model-fragments, - storefront, - travis-web, - ] - include: - - partner: storefront - continue-on-error: true - - partner: factory-guy - continue-on-error: true - - partner: travis-web - continue-on-error: true - - partner: ilios-frontend - continue-on-error: true - - partner: model-fragments - continue-on-error: true - - partner: ember-observer - continue-on-error: true - - partner: ember-m3 - continue-on-error: true - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup - with: - restore-broccoli-cache: true - install: true - build-addons: true - - name: Generate package tarballs - run: node ./scripts/packages-for-commit.js - - name: Run Tests - id: test-partner - timeout-minutes: 16 - env: - CI: true - run: pnpm test-external:${{ matrix.partner }} - continue-on-error: ${{ matrix['continue-on-error'] == true }} - - name: Check on failures - if: ${{ matrix['continue-on-error'] == true && steps.test-partner.outcome == 'success' }} - run: exit 1 diff --git a/.github/workflows/perf-check.yml b/.github/workflows/perf-check.yml index a1c19f44d20..c114ad4754c 100644 --- a/.github/workflows/perf-check.yml +++ b/.github/workflows/perf-check.yml @@ -9,6 +9,11 @@ on: - synchronize - ready_for_review +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + concurrency: group: perf-${{ github.head_ref || github.ref_name }} cancel-in-progress: true @@ -19,7 +24,7 @@ jobs: name: 'Performance Checks' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin main --depth=1 @@ -33,8 +38,8 @@ 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@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + - uses: actions/setup-node@v4 with: node-version: 19.x cache: 'pnpm' @@ -44,6 +49,7 @@ jobs: experiment-serve-command: pnpm --filter performance-test-app exec ember s --path dist-experiment --port 4201 control-build-command: pnpm install && pnpm --filter performance-test-app exec ember build -e production --output-path dist-control --suppress-sizes control-serve-command: pnpm --filter performance-test-app exec ember s --path dist-control + control-sha: origin/main sample-timeout: 60 use-pnpm: true scenarios: | diff --git a/.github/workflows/perf-over-release.yml b/.github/workflows/perf-over-release.yml index 1c0cb0c1d46..7dad11a0722 100644 --- a/.github/workflows/perf-over-release.yml +++ b/.github/workflows/perf-over-release.yml @@ -9,6 +9,11 @@ on: - synchronize - ready_for_review +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + concurrency: group: perf-release-${{ github.head_ref || github.ref_name }} cancel-in-progress: true @@ -19,7 +24,7 @@ jobs: name: 'Performance Check Against Release' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 with: fetch-depth: 3 - run: git fetch origin release --depth=1 @@ -33,8 +38,8 @@ 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@fe02b34f77f8bc703788d5817da081398fad5dd2 # tag=v4.0.0 - - uses: actions/setup-node@v3 + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + - uses: actions/setup-node@v4 with: node-version: 19.x cache: 'pnpm' diff --git a/.github/workflows/release_promote-lts.yml b/.github/workflows/release_promote-lts.yml new file mode 100644 index 00000000000..251e72fc0ee --- /dev/null +++ b/.github/workflows/release_promote-lts.yml @@ -0,0 +1,55 @@ +name: 0. Release > Promote LTS + +on: + workflow_dispatch: + inputs: + version: + description: 'The existing version to promote (e.g. `4.0.0`)' + type: string + channel: + description: 'The NPM Distribution Tag (e.g. `lts` or `lts-4-8`)' + type: string + update-branch: + description: 'Whether to update the associated LTS branch to the same commit as the tag' + default: true + type: boolean + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + +jobs: + release: + name: Run Release Script + runs-on: ubuntu-latest + environment: deployment + steps: + - name: Enforce Branch + # Note: we always checkout main in actions/checkout, but this enforces + # good hygiene. + if: github.ref != 'refs/heads/main' + run: | + echo "Releases may only be performed from the main branch." + exit 1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + with: + fetch-depth: 1 + fetch-tags: true + show-progress: false + token: ${{ secrets.GH_DEPLOY_TOKEN }} + - run: git fetch origin --tags --depth=1 + - uses: ./.github/actions/setup + with: + install: true + repo-token: ${{ secrets.GH_DEPLOY_TOKEN }} + - name: Make sure git user is setup + run: | + git config --local user.email ${{ secrets.GH_DEPLOY_EMAIL }} + git config --local user.name ${{ secrets.GH_DEPLOY_NAME }} + - name: Publish with script + run: bun release exec promote --v=${{ github.event.inputs.version }} --t=${{ github.event.inputs.channel }} --u=${{ github.event.inputs.update-branch }} + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/release_publish-beta.yml b/.github/workflows/release_publish-beta.yml new file mode 100644 index 00000000000..d4802e64dd2 --- /dev/null +++ b/.github/workflows/release_publish-beta.yml @@ -0,0 +1,92 @@ +name: 0. Release > Beta + +on: + workflow_dispatch: + inputs: + # This input is used to determine whether to start/continue a beta-cycle vs mirror from canary. + # + # A beta-cycle "forks" from canary. It starts by updating the beta branch to the current state + # of main (canary). Thereafter any updates to the beta branch are cherry-picked from main or PR'd + # to the beta branch. + # + # The (default) mirror approach instead directly copies the canary release to the beta branch + # each time. This is useful when the changes in canary are relatively minor or safe to release + # and + # and then publishing a beta release. A mirror is a direct copy of the canary release. + kind: + description: 'Whether to start/continue a beta-cycle vs mirror from canary' + required: true + default: 'mirror' + type: choice + options: + - beta-cycle # start or continue a beta-cycle. + - mirror # mirror code from canary. This is the default. + # At cycle start we must always reset the beta branch to main. + is-cycle-start: + description: 'Whether this is the start of a new release cycle (either kind)' + required: true + default: false + type: boolean + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + +jobs: + release: + name: Run publish script + runs-on: ubuntu-latest + environment: deployment + steps: + - name: Enforce Branch + # Note: we always checkout beta in actions/checkout, but this enforces + # good hygiene. + if: github.ref != 'refs/heads/main' + run: | + echo "Releases may only be performed from the main branch." + exit 1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + with: + fetch-tags: true + show-progress: false + token: ${{ secrets.GH_DEPLOY_TOKEN }} + fetch-depth: 3 + ref: beta + - run: git fetch origin main --depth=1 + - run: git fetch origin --tags --depth=1 + - name: Make sure git user is setup + run: | + git config --local user.email ${{ secrets.GH_DEPLOY_EMAIL }} + git config --local user.name ${{ secrets.GH_DEPLOY_NAME }} + - name: Reset the Beta Branch + if: github.event.inputs.kind == 'mirror' || github.event.inputs.is-cycle-start == 'true' + run: git reset --hard origin/main && git push origin beta -f + - uses: ./.github/actions/setup + with: + install: true + repo-token: ${{ secrets.GH_DEPLOY_TOKEN }} + - name: Get most recent beta version + id: version + if: github.event.inputs.kind == 'mirror' + run: echo "value=$(bun --silent release latest beta)" >> $GITHUB_OUTPUT + - name: Publish New Release + # For beta-cycle we always increment from the branch state + # For mirror we increment from the last beta version, unless it's start of a new cycle. + if: github.event.inputs.kind == 'beta-cycle' || github.event.inputs.is-cycle-start == 'true' + run: bun release exec publish beta + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + - name: Publish New Mirror Release + if: github.event.inputs.kind == 'mirror' + run: bun release exec publish beta --from=${{ steps.version.outputs.value }} + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: tarballs + path: tmp/tarballs/**/*.tgz diff --git a/.github/workflows/release_publish-canary.yml b/.github/workflows/release_publish-canary.yml new file mode 100644 index 00000000000..4b0d856feac --- /dev/null +++ b/.github/workflows/release_publish-canary.yml @@ -0,0 +1,67 @@ +name: 0. Release > Canary + +on: + workflow_dispatch: + inputs: + increment: + description: 'Type of Version Bump To Perform' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + schedule: + - cron: '0 20 * * 2' # weekly (Tuesday) 12 PM PST + - cron: '0 20 * * 5' # weekly (Friday) 12 PM PST + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + +jobs: + release: + name: Run publish script + runs-on: ubuntu-latest + environment: deployment + steps: + - name: Enforce Branch + # Note: we always checkout main in actions/checkout, but this enforces + # good hygiene. + if: github.ref != 'refs/heads/main' + run: | + echo "Releases may only be performed from the main branch." + exit 1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + with: + fetch-depth: 1 + fetch-tags: true + show-progress: false + token: ${{ secrets.GH_DEPLOY_TOKEN }} + ref: main + - name: Check should run if HEAD is untagged + run: | + echo "HEAD is $(git name-rev --tags --name-only $(git rev-parse HEAD))" + if [[ "$(git name-rev --tags --name-only $(git rev-parse HEAD))" != "undefined" ]]; then + exit 1 + fi + - uses: ./.github/actions/setup + with: + install: true + repo-token: ${{ secrets.GH_DEPLOY_TOKEN }} + - name: Make sure git user is setup + run: | + git config --local user.email ${{ secrets.GH_DEPLOY_EMAIL }} + git config --local user.name ${{ secrets.GH_DEPLOY_NAME }} + - name: Publish with script + run: bun release exec canary --increment=${{ github.event.inputs.increment }} + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: tarballs + path: tmp/tarballs/**/*.tgz diff --git a/.github/workflows/release_publish-lts.yml b/.github/workflows/release_publish-lts.yml new file mode 100644 index 00000000000..7d55531f2bf --- /dev/null +++ b/.github/workflows/release_publish-lts.yml @@ -0,0 +1,64 @@ +name: 0. Release > LTS + +on: + workflow_dispatch: + inputs: + branch: + description: 'The branch to publish from, e.g. `lts-4-12`' + required: true + type: string + channel: + description: 'The NPM Distribution Tag. `lts` for current lts. `lts-prev` for e.g. `lts-4-8`' + type: option + default: 'lts' + required: true + options: + - lts + - lts-prev + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + +jobs: + release: + name: Run publish script + runs-on: ubuntu-latest + environment: deployment + steps: + - name: Enforce Branch + # Note: we always checkout the correct lts branch in actions/checkout, but this enforces + # good hygiene. + if: github.ref != 'refs/heads/main' + run: | + echo "Releases may only be performed from the main branch." + exit 1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + with: + fetch-tags: true + show-progress: false + token: ${{ secrets.GH_DEPLOY_TOKEN }} + fetch-depth: 25 + ref: ${{ github.event.inputs.source-branch }} + - run: git fetch origin --tags --depth=1 + - name: Make sure git user is setup + run: | + git config --local user.email ${{ secrets.GH_DEPLOY_EMAIL }} + git config --local user.name ${{ secrets.GH_DEPLOY_NAME }} + - uses: ./.github/actions/setup + with: + install: true + repo-token: ${{ secrets.GH_DEPLOY_TOKEN }} + - name: Publish New LTS Release + # We always increment from the branch state + run: bun release publish ${{ github.event.inputs.channel }} + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + GITHUB_AUTH: ${{ secrets.GH_DEPLOY_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: tarballs + path: tmp/tarballs/**/*.tgz diff --git a/.github/workflows/release_publish-stable.yml b/.github/workflows/release_publish-stable.yml new file mode 100644 index 00000000000..db8ce0196b7 --- /dev/null +++ b/.github/workflows/release_publish-stable.yml @@ -0,0 +1,103 @@ +name: 0. Release > Stable + +on: + workflow_dispatch: + inputs: + source-branch: + description: 'If starting a new cycle, or reversioning, the source branch to update the release branch from' + required: false + default: 'beta' + type: choice + options: + - beta # promotes beta to stable + - main # promotes canary to stable + - release # re-releases a stable version + # At cycle start we must always reset the release branch to beta. + is-cycle-start: + description: 'Whether this is the start of a new release cycle' + required: true + default: false + type: boolean + # downversion e.g. 5.4.0-alpha.1 => 5.3.1 happens when we use a canary, beta or later release to hotfix a stable + # upversion e.g. 5.3.1 => 5.4.0 happens when we re-release an existing stable as a new minor/major + # examples: + # Upversion: 5.3.1 => 5.4.0 + # from-version: 5.3.1 + # increment: minor + # Downversion: 5.4.0-alpha.1 => 5.3.1 + # from-version: 5.3.0 + # increment: patch + from-version: + description: 'When upversioning or downversioning, the version from which to increment to get the version number for the release' + type: string + increment: + description: 'Type of Version Bump To Perform (only used when upversioning or downversioning)' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +env: + TURBO_API: http://127.0.0.1:9080 + TURBO_TOKEN: this-is-not-a-secret + TURBO_TEAM: myself + +jobs: + release: + name: Perform Release + runs-on: ubuntu-latest + environment: deployment + steps: + - name: Enforce Branch + # Note: we always checkout release in actions/checkout, but this enforces + # good hygiene. + if: github.ref != 'refs/heads/main' + run: | + echo "Releases may only be performed from the main branch." + exit 1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 + with: + fetch-tags: true + show-progress: false + token: ${{ secrets.GH_DEPLOY_TOKEN }} + fetch-depth: 3 + ref: release + ## Ensure we have a full copy of the source branch + - run: git fetch origin ${{ github.event.inputs.source-branch }} + - run: git fetch origin --tags --depth=1 + - name: Make sure git user is setup + run: | + git config --local user.email ${{ secrets.GH_DEPLOY_EMAIL }} + git config --local user.name ${{ secrets.GH_DEPLOY_NAME }} + - name: Reset the Release Branch + if: github.event.inputs.source-branch != 'release' && (github.event.inputs.is-cycle-start == 'true' || github.event.inputs.from-version != null) + run: git reset --hard origin/${{ github.event.inputs.source-branch }} && git push origin release -f + - uses: ./.github/actions/setup + with: + install: true + repo-token: ${{ secrets.GH_DEPLOY_TOKEN }} + - name: Publish New Release + # If we are not reversioning + # Then we do the default patch increment from the current branch state. + # This handles both start-of-cycle and bugfix releases. + if: github.event.inputs.from-version == null + run: bun release publish release + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + - name: Publish New Release (Reversion) + # If we are reversioning + # Then we increment from the branch with the supplied increment + # This handles both upversioning and downversioning + if: github.event.inputs.from-version != null + run: bun release publish release --from=${{ github.event.inputs.from-version }} --increment=${{ github.event.inputs.increment }} + env: + FORCE_COLOR: 2 + CI: true + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index baebd34406f..61f344eb1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,16 +7,8 @@ concat-stats-for dist dist-* tmp -packages/tracking/addon -packages/request/addon -packages/request-utils/addon -packages/store/addon -packages/adapter/addon -packages/serializer/addon -packages/model/addon -packages/json-api/addon -packages/graph/addon -packages/legacy-compat/addon +unstable-preview-types +packages/*/addon # dependencies bower_components @@ -24,15 +16,18 @@ node_modules scripts/asset-size-tracking/current-data.json # misc +.turbo/ .env* .pnp* +.prettier-cache .sass-cache -/.eslintcache +.eslintcache /onnect.lock coverage/* libpeerconnection.log npm-debug.log* testem.log +test-execution-*.json yarn-error.log .broccoli-cache tsconfig.tsbuildinfo @@ -58,6 +53,6 @@ benchmarks/results/*.json /packages/*/DEBUG /tests/*/DEBUG -.vscode/ +!.vscode/ .idea/ *.iml diff --git a/.npmrc b/.npmrc index b504ef1f941..32d24cf300f 100644 --- a/.npmrc +++ b/.npmrc @@ -11,9 +11,15 @@ hoist-pattern[]=*node-fetch* # we want true but cannot use true until the below issue is fixed # https://github.com/pnpm/pnpm/issues/5340 -strict-peer-dependencies=false +strict-peer-dependencies=true auto-install-peers=false # probably apps should set this to true, but we need to test with it false to be sure we aren't the bad citizen -dedupe-peer-dependents=false # this currently introduces more bugs than it fixes +dedupe-peer-dependents=true # this currently introduces more bugs than it fixes resolve-peers-from-workspace-root=false # if its not declared we don't want it resolved: ensure tests are truly isolated save-workspace-protocol=rolling resolution-mode=highest +dedupe-direct-deps=true +child-concurrency=10 +ignore-dep-scripts=true +dedupe-injected-deps=false +hoist-workspace-packages=false +enable-pre-post-scripts=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..983164e8e09 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,28 @@ +# In addition to gitignore... + +# generated files +pnpm-lock.yaml +dist +unstable-preview-types +preview-types + +# we disagree with prettier and we are even more opinionated than they are +*.hbs +*.html +*.md + +# these files would be YUGE if we prettified them +MOCK_DATA.json +.mock-cache/ + +# unconventional +blueprints/ +vendor/ +!tests/blueprints/ + +# prettier is reporting syntax errors in these +*.yml + +# we don't really care about these +main/public/ +tests/fastboot/public/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 2dbbfb3c230..00000000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -singleQuote: true -trailingComma: 'es5' -printWidth: 120 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000000..d03704c0786 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,28 @@ +module.exports = { + trailingComma: 'es5', + printWidth: 120, + plugins: ['prettier-plugin-ember-template-tag'], + overrides: [ + { + files: '*.{js,ts,cjs,cts,mjs,mts}', + options: { + singleQuote: true, + }, + }, + { + files: ['*.hbs'], + options: { + singleQuote: false, + }, + }, + { + files: ['*.gjs', '*.gts'], + options: { + parser: 'ember-template-tag', + singleQuote: true, + templateSingleQuote: false, + trailingComma: 'es5', + }, + }, + ], +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..f5b65b337a5 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,53 @@ +{ + "recommendations": [ + "bierner.markdown-preview-github-styles", // Markdown Preview Github Styling + "eamodio.gitlens", // GitLens + "visualstudioexptteam.vscodeintellicode", // IntelliCode + "typed-ember.glint-vscode", // Glint (Ember Template Types) + + // Includes + // - syntax highlighting + // - language server + // - eslint + // - stylelint + // - prettier + // - editorconfig + // + // https://marketplace.visualstudio.com/items?itemName=EmberTooling.emberjs + "embertooling.emberjs", + // But if someone removes one, just recommending the extension pack isn't enough to + // recommend the individual extensions again + "embertooling.vscode-ember", + "lifeart.vscode-glimmer-syntax", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint" + + // Warns if any of the below are present + // Disabled because it doesn't work. + // See: https://github.com/SoulcodeAgency/vscode-unwanted-extensions/issues/9 if someone wants to try creating a version of this that does work. + // Purpose: warn if any of the extensions below are installed. + //"Soulcode.vscode-unwanted-extensions" + ], + "unwantedRecommendations": [ + // None of these are particularly useful for Ember development + "googlecloudtools.cloudcode", // Google Cloud Code + "firefox-devtools.vscode-firefox-debug", // Firefox Debug + "ms-edgedevtools.vscode-edge-devtools", // Microsoft Edge DevTools for VS Code + "ms-kubernetes-tools.vscode-kubernetes-tools", // Kubernetes + + // out of date gjs and gts syntax highlighting. + "chiragpat.vscode-glimmer", + // old version of the language server built in to embertooling + "lifeart.vscode-ember-unstable", + // out of date fork for linkedin + "suchitadoshi1987.vscode-ember-experimental", + // out of date + "dhedgecock.ember-syntax", + // theme, unrelated to ember + "jgibson.ember-color-theme", + // don't use jest + "Orta.vscode-jest" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..6858a777056 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,52 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "request": "launch", + "name": "Debug Bun", + + // The path to a JavaScript or TypeScript file to run. + "program": "${file}", + + // The arguments to pass to the program, if any. + "args": [], + + // The working directory of the program. + "cwd": "${workspaceFolder}", + + // The environment variables to pass to the program. + "env": {}, + + // If the environment variables should not be inherited from the parent process. + "strictEnv": false, + + // If the program should be run in watch mode. + // This is equivalent to passing `--watch` to the `bun` executable. + // You can also set this to "hot" to enable hot reloading using `--hot`. + "watchMode": false, + + // If the debugger should stop on the first line of the program. + "stopOnEntry": false, + + // If the debugger should be disabled. (for example, breakpoints will not be hit) + "noDebug": false, + + // The path to the `bun` executable, defaults to your `PATH` environment variable. + "runtime": "bun", + + // The arguments to pass to the `bun` executable, if any. + // Unlike `args`, these are passed to the executable itself, not the program. + "runtimeArgs": [] + }, + { + "type": "bun", + "request": "attach", + "name": "Attach to Bun", + + // The URL of the WebSocket inspector to attach to. + // This value can be retrieved by using `bun --inspect`. + "url": "ws://localhost:6499/" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..d43d5af120c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,54 @@ +{ + "eslint.format.enable": true, + "eslint.workingDirectories": [{ "mode": "auto" }, "packages/*", "tests/*"], + "eslint.useFlatConfig": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "files.associations": { + "turbo.json": "jsonc" + }, + "typescript.preferences.autoImportSpecifierExcludeRegexes": [ + "^@ember/-internals/.*$", + "^@ember-data/*/-private(.*?)$", + "^@warp-drive/*/-private(.*?)$", + "^ember-data/.*$", + "^ember-data$" + ], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[handlebars]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[glimmer-js]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[glimmer-ts]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[markdown]": { + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4 + }, + "typescript.preferences.importModuleSpecifier": "project-relative", + "bun.debugTerminal.enabled": true, + "eslint.debug": true, + "eslint.runtime": "node", + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000000..5c3f2f6d9cb --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,34 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "pnpm: lint", + "type": "shell", + "command": "pnpm lint", + + "problemMatcher": { + "owner": "pnpm-lint", + "fileLocation": ["absolute"], + "background": { + "activeOnStart": true, + "beginsPattern": "^Scope: .* workspace projects$", + "endsPattern": "^Summary: .* fails, .* passes$" + }, + "pattern": [ + { + "regexp": "^.+lint: (\\/.+)$", + "file": 1 + }, + { + "regexp": "^.+lint:\\s+(\\d+):(\\d+)\\s+(\\w+)\\s+(.+)$", + "line": 1, + "column": 2, + "severity": 3, + "message": 4, + "loop": true + } + ] + } + } + ] +} diff --git a/@types/@ember/array/-private/enumerable.d.ts b/@types/@ember/array/-private/enumerable.d.ts deleted file mode 100644 index 9952c756467..00000000000 --- a/@types/@ember/array/-private/enumerable.d.ts +++ /dev/null @@ -1,167 +0,0 @@ -import type NativeArray from '@ember/array/-private/native-array'; -import type ComputedProperty from '@ember/object/computed'; -import type Mixin from '@ember/object/mixin'; -/** - * This mixin defines the common interface implemented by enumerable objects - * in Ember. Most of these methods follow the standard Array iteration - * API defined up to JavaScript 1.8 (excluding language-specific features that - * cannot be emulated in older versions of JavaScript). - */ -interface Enumerable { - /** - * Helper method returns the first object from a collection. This is usually - * used by bindings and other parts of the framework to extract a single - * object if the enumerable contains only one item. - */ - firstObject: T | undefined; - /** - * Helper method returns the last object from a collection. If your enumerable - * contains only one object, this method should always return that object. - * If your enumerable is empty, this method should return `undefined`. - */ - lastObject: T | undefined; - /** - * @deprecated Use `Enumerable#includes` instead. - */ - contains(obj: T): boolean; - /** - * Iterates through the enumerable, calling the passed function on each - * item. This method corresponds to the `forEach()` method defined in - * JavaScript 1.6. - */ - forEach: T[]['forEach']; - /** - * Alias for `mapBy` - */ - getEach(key: string): unknown[]; - /** - * Sets the value on the named property for each member. This is more - * ergonomic than using other methods defined on this helper. If the object - * implements Ember.Observable, the value will be changed to `set(),` otherwise - * it will be set directly. `null` objects are skipped. - */ - setEach(key: string, value: unknown): unknown; - /** - * Maps all of the items in the enumeration to another value, returning - * a new array. This method corresponds to `map()` defined in JavaScript 1.6. - */ - map: T[]['map']; - /** - * Similar to map, this specialized function returns the value of the named - * property on all items in the enumeration. - */ - mapBy(key: string): unknown[]; - /** - * Returns an array with all of the items in the enumeration that the passed - * function returns true for. This method corresponds to `filter()` defined in - * JavaScript 1.6. - */ - filter: T[]['filter']; - /** - * Returns an array with all of the items in the enumeration where the passed - * function returns false. This method is the inverse of filter(). - */ - reject(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): NativeArray; - /** - * Returns an array with just the items with the matched property. You - * can pass an optional second argument with the target value. Otherwise - * this will match any property that evaluates to `true`. - */ - filterBy(key: string, value?: unknown): NativeArray; - /** - * Returns an array with the items that do not have truthy values for - * key. You can pass an optional second argument with the target value. Otherwise - * this will match any property that evaluates to false. - */ - rejectBy(key: string, value?: unknown): NativeArray; - /** - * Returns the first item in the array for which the callback returns true. - * This method works similar to the `filter()` method defined in JavaScript 1.6 - * except that it will stop working on the array once a match is found. - */ - find: T[]['find']; - /** - * Returns the first item with a property matching the passed value. You - * can pass an optional second argument with the target value. Otherwise - * this will match any property that evaluates to `true`. - */ - findBy(key: string, value?: unknown): T | undefined; - /** - * Returns `true` if the passed function returns true for every item in the - * enumeration. This corresponds with the `every()` method in JavaScript 1.6. - */ - every: T[]['every']; - /** - * Returns `true` if the passed property resolves to the value of the second - * argument for all items in the enumerable. This method is often simpler/faster - * than using a callback. - */ - isEvery(key: string, value?: unknown): boolean; - /** - * Returns `true` if the passed function returns true for any item in the - * enumeration. - */ - any(callback: (value: T, index: number, array: T[]) => boolean, target?: {}): boolean; - /** - * Returns `true` if the passed property resolves to the value of the second - * argument for any item in the enumerable. This method is often simpler/faster - * than using a callback. - */ - isAny(key: string, value?: unknown): boolean; - /** - * This will combine the values of the enumerator into a single value. It - * is a useful way to collect a summary value from an enumeration. This - * corresponds to the `reduce()` method defined in JavaScript 1.8. - */ - reduce: T[]['reduce']; - /** - * Invokes the named method on every object in the receiver that - * implements it. This method corresponds to the implementation in - * Prototype 1.6. - */ - invoke(methodName: keyof I, ...args: unknown[]): unknown[]; - /** - * Simply converts the enumerable into a genuine array. The order is not - * guaranteed. Corresponds to the method implemented by Prototype. - */ - toArray(): T[]; - /** - * Returns a copy of the array with all `null` and `undefined` elements removed. - */ - compact(): NativeArray>; - /** - * Returns a new enumerable that excludes the passed value. The default - * implementation returns an array regardless of the receiver type. - * If the receiver does not contain the value it returns the original enumerable. - */ - without(value: T): NativeArray; - /** - * Returns a new enumerable that contains only unique values. The default - * implementation returns an array regardless of the receiver type. - */ - uniq(): NativeArray; - /** - * Converts the enumerable into an array and sorts by the keys - * specified in the argument. - */ - sortBy(...properties: string[]): NativeArray; - /** - * Returns a new enumerable that contains only items containing a unique property value. - * The default implementation returns an array regardless of the receiver type. - */ - uniqBy(property: string): NativeArray; - uniqBy(callback: (value: T) => unknown): NativeArray; - /** - * Returns `true` if the passed object can be found in the enumerable. - */ - includes(searchElement: T, fromIndex?: number): boolean; - /** - * This is the handler for the special array content property. If you get - * this property, it will return this. If you set this property to a new - * array, it will replace the current content. - */ - '[]': ComputedProperty; -} -declare const Enumerable: Mixin>; - -export default Enumerable; diff --git a/@types/@ember/array/-private/mutable-enumerable.d.ts b/@types/@ember/array/-private/mutable-enumerable.d.ts deleted file mode 100644 index f6f7406009c..00000000000 --- a/@types/@ember/array/-private/mutable-enumerable.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type Enumerable from '@ember/array/-private/enumerable'; -import type Mixin from '@ember/object/mixin'; - -/** - * This mixin defines the API for modifying generic enumerables. These methods - * can be applied to an object regardless of whether it is ordered or - * unordered. - */ -interface MutableEnumerable extends Enumerable { - /** - * __Required.__ You must implement this method to apply this mixin. - */ - addObject(object: T): this; - /** - * Adds each object in the passed enumerable to the receiver. - */ - addObjects(objects: T[] | Enumerable): this; - /** - * __Required.__ You must implement this method to apply this mixin. - */ - removeObject(object: T): this; - /** - * Removes each object in the passed enumerable from the receiver. - */ - removeObjects(objects: T[] | Enumerable): this; -} -declare const MutableEnumerable: Mixin>; -export default MutableEnumerable; diff --git a/@types/@ember/array/-private/native-array.d.ts b/@types/@ember/array/-private/native-array.d.ts deleted file mode 100644 index 3f49f70f735..00000000000 --- a/@types/@ember/array/-private/native-array.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import MutableArray from '@ember/array/mutable'; -import Copyable from '@ember/object/-private/copyable'; -import type Mixin from '@ember/object/mixin'; -import Observable from '@ember/object/observable'; - -// Get an alias to the global Array type to use in inner scope below. -type GlobalArray = T[]; - -/** - * The NativeArray mixin contains the properties needed to make the native - * Array support Ember.MutableArray and all of its dependent APIs. Unless you - * have `EmberENV.EXTEND_PROTOTYPES` or `EmberENV.EXTEND_PROTOTYPES.Array` set to - * false, this will be applied automatically. Otherwise you can apply the mixin - * at anytime by calling `Ember.NativeArray.apply(Array.prototype)`. - */ -interface NativeArray extends GlobalArray, MutableArray, Observable, Copyable { - /** - * __Required.__ You must implement this method to apply this mixin. - */ - length: number; -} -declare const NativeArray: Mixin>; - -export default NativeArray; diff --git a/@types/@ember/array/index.d.ts b/@types/@ember/array/index.d.ts deleted file mode 100644 index 087cbe54ca3..00000000000 --- a/@types/@ember/array/index.d.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Type definitions for non-npm package @ember/array 3.16 -// Project: https://emberjs.com/api/ember/3.16/modules/@ember%2Farray -// Definitions by: Mike North -// Chris Krycho -// Dan Freeman -// James C. Davis -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 3.7 - -import Enumerable from '@ember/array/-private/enumerable'; -import type NativeArray from '@ember/array/-private/native-array'; -import type ComputedProperty from '@ember/object/computed'; - -/** - * This module implements Observer-friendly Array-like behavior. This mixin is picked up by the - * Array class as well as other controllers, etc. that want to appear to be arrays. - */ -interface EmberArray extends Enumerable { - /** - * __Required.__ You must implement this method to apply this mixin. - */ - length: number | ComputedProperty; - /** - * Returns the object at the given `index`. If the given `index` is negative - * or is greater or equal than the array length, returns `undefined`. - */ - objectAt(idx: number): T | undefined; - /** - * This returns the objects at the specified indexes, using `objectAt`. - */ - // tslint:disable-next-line:array-type - objectsAt(indexes: number[]): Array; - /** - * Returns a new array that is a slice of the receiver. This implementation - * uses the observable array methods to retrieve the objects for the new - * slice. - */ - slice(beginIndex?: number, endIndex?: number): T[]; - /** - * Returns the index of the given object's first occurrence. - * If no `startAt` argument is given, the starting location to - * search is 0. If it's negative, will count backward from - * the end of the array. Returns -1 if no match is found. - */ - indexOf(searchElement: T, fromIndex?: number): number; - /** - * Returns the index of the given object's last occurrence. - * If no `startAt` argument is given, the search starts from - * the last position. If it's negative, will count backward - * from the end of the array. Returns -1 if no match is found. - */ - lastIndexOf(searchElement: T, fromIndex?: number): number; - /** - * Adds an array observer to the receiving array. The array observer object - * normally must implement two methods: - */ - addArrayObserver(target: {}, opts: {}): this; - /** - * Removes an array observer from the object if the observer is current - * registered. Calling this method multiple times with the same object will - * have no effect. - */ - removeArrayObserver(target: {}, opts: {}): this; - /** - * Becomes true whenever the array currently has observers watching changes - * on the array. - */ - hasArrayObservers: boolean | ComputedProperty; - /** - * If you are implementing an object that supports `Ember.Array`, call this - * method just before the array content changes to notify any observers and - * invalidate any related properties. Pass the starting index of the change - * as well as a delta of the amounts to change. - */ - arrayContentWillChange(startIdx: number, removeAmt: number, addAmt: number): this; - /** - * If you are implementing an object that supports `Ember.Array`, call this - * method just after the array content changes to notify any observers and - * invalidate any related properties. Pass the starting index of the change - * as well as a delta of the amounts to change. - */ - arrayContentDidChange(startIdx: number, removeAmt: number, addAmt: number): this; - /** - * Returns a special object that can be used to observe individual properties - * on the array. Just get an equivalent property on this object and it will - * return an enumerable that maps automatically to the named key on the - * member objects. - */ - '@each': ComputedProperty; -} - -export const NativeArray; - -export default EmberArray; - -/** - * Creates an `Ember.NativeArray` from an Array like object. - * Does not modify the original object's contents. Ember.A is not needed if - * `EmberENV.EXTEND_PROTOTYPES` is `true` (the default value). However, - * it is recommended that you use Ember.A when creating addons for - * ember or when you can not guarantee that `EmberENV.EXTEND_PROTOTYPES` - * will be `true`. - */ -export function A(arr?: T[]): NativeArray; - -/** - * Returns true if the passed object is an array or Array-like. - */ -export function isArray(obj: unknown): obj is ArrayLike; diff --git a/@types/@ember/array/mutable.d.ts b/@types/@ember/array/mutable.d.ts deleted file mode 100644 index bb6900b2034..00000000000 --- a/@types/@ember/array/mutable.d.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type EmberArray from '@ember/array'; -import type Enumerable from '@ember/array/-private/enumerable'; -import MutableEnumerable from '@ember/array/-private/mutable-enumerable'; -import type Mixin from '@ember/object/mixin'; - -/** - * This mixin defines the API for modifying array-like objects. These methods - * can be applied only to a collection that keeps its items in an ordered set. - * It builds upon the Array mixin and adds methods to modify the array. - * One concrete implementations of this class include ArrayProxy. - */ -interface MutableArray extends EmberArray, MutableEnumerable { - /** - * __Required.__ You must implement this method to apply this mixin. - */ - replace(idx: number, amt: number, objects?: T[] | null): void; - /** - * Remove all elements from the array. This is useful if you - * want to reuse an existing array without having to recreate it. - */ - clear(): this; - /** - * This will use the primitive `replace()` method to insert an object at the - * specified index. - */ - insertAt(idx: number, object: {}): this; - /** - * Remove an object at the specified index using the `replace()` primitive - * method. You can pass either a single index, or a start and a length. - */ - removeAt(start: number, len?: number): this; - /** - * Push the object onto the end of the array. Works just like `push()` but it - * is KVO-compliant. - */ - pushObject(obj: T): T; - /** - * Add the objects in the passed numerable to the end of the array. Defers - * notifying observers of the change until all objects are added. - */ - pushObjects(objects: T[] | Enumerable): this; - /** - * Pop object from array or nil if none are left. Works just like `pop()` but - * it is KVO-compliant. - */ - popObject(): T; - /** - * Shift an object from start of array or nil if none are left. Works just - * like `shift()` but it is KVO-compliant. - */ - shiftObject(): T; - /** - * Unshift an object to start of array. Works just like `unshift()` but it is - * KVO-compliant. - */ - unshiftObject(obj: T): T; - /** - * Adds the named objects to the beginning of the array. Defers notifying - * observers until all objects have been added. - */ - unshiftObjects(objects: T[] | Enumerable): this; - /** - * Reverse objects in the array. Works just like `reverse()` but it is - * KVO-compliant. - */ - reverseObjects(): this; - /** - * Replace all the receiver's content with content of the argument. - * If argument is an empty array receiver will be cleared. - */ - setObjects(objects: T[] | EmberArray): this; -} -declare const MutableArray: Mixin>; - -export default MutableArray; diff --git a/@types/@ember/array/proxy.d.ts b/@types/@ember/array/proxy.d.ts deleted file mode 100644 index 86b250d02d0..00000000000 --- a/@types/@ember/array/proxy.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import MutableArray from '@ember/array/mutable'; -import EmberObject from '@ember/object'; -import type ComputedProperty from '@ember/object/computed'; - -interface EmberArrayLike { - length: number | ComputedProperty; - objectAt(idx: number): T | undefined; -} - -/** - * An ArrayProxy wraps any other object that is a native or Ember `Array` - * (checked with [`Ember.isArray`](https://api.emberjs.com/ember/release/functions/@ember%2Farray/isArray)), - * forwarding all requests. This makes it very useful for a number of - * binding use cases or other cases where being able to swap out the - * underlying array is useful. - * - * NOTE: Attempting to mutate the underlying content of an object that - * is not a `MutableArray` (e.g. a native Javascript Array) may not - * behave as expected. [`Ember.A`](https://api.emberjs.com/ember/release/functions/@ember%2Farray/A) - * may be used in this case. - */ -interface ArrayProxy extends MutableArray { - content: ContentType[] | EmberArrayLike; - /** - * Should actually retrieve the object at the specified index from the - * content. You can override this method in subclasses to transform the - * content item to something new. - */ - objectAtContent(idx: number): MaterializedType | undefined; -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -declare class ArrayProxy extends EmberObject {} -export default ArrayProxy; diff --git a/@types/@ember/debug/index.d.ts b/@types/@ember/debug/index.d.ts deleted file mode 100644 index 32f0b3c63b7..00000000000 --- a/@types/@ember/debug/index.d.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/pull/52397 - to unblock while waiting for a new version. -*/ -// Type definitions for non-npm package @ember/debug 3.24 -// Project: https://emberjs.com/api/ember/3.24/modules/@ember%2Fdebug -// Definitions by: Mike North -// Chris Krycho -// Dan Freeman -// James C. Davis -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 3.7 - -/** - * Define an assertion that will throw an exception if the condition is not met. - */ -export function assert(desc: string): never; -export function assert(desc: string, test: unknown): asserts test; - -/** - * Display a debug notice. - */ -export function debug(message: string): void; - -/** - * Convenience method to inspect an object. This method will attempt to - * convert the object into a useful string description. - */ -export function inspect(obj: any): string; -/** - * Allows for runtime registration of handler functions that override the default deprecation behavior. - * Deprecations are invoked by calls to [Ember.deprecate](http://emberjs.com/api/classes/Ember.html#method_deprecate). - * The following example demonstrates its usage by registering a handler that throws an error if the - * message contains the word "should", otherwise defers to the default handler. - */ -export function registerDeprecationHandler( - handler: (message: string, options: { id: string; until: string }, next: () => void) => void -): void; -/** - * Allows for runtime registration of handler functions that override the default warning behavior. - * Warnings are invoked by calls made to [Ember.warn](http://emberjs.com/api/classes/Ember.html#method_warn). - * The following example demonstrates its usage by registering a handler that does nothing overriding Ember's - * default warning behavior. - */ -export function registerWarnHandler( - handler: ( - message: string, - options: { id: string }, - next: (message?: string, options?: { id: string }) => void - ) => void -): void; - -/** - * Run a function meant for debugging. - */ -export function runInDebug(func: () => any): void; - -/** - * Display a warning with the provided message. - */ -export function warn(message: string, test: boolean, options: { id: string }): void; -export function warn(message: string, options: { id: string }): void; -/** - * @deprecated Missing deprecation options: https://emberjs.com/deprecations/v2.x/#toc_ember-debug-function-options - */ -export function warn(message: string, test: boolean, options?: { id?: string }): void; -/** - * @deprecated Missing deprecation options: https://emberjs.com/deprecations/v2.x/#toc_ember-debug-function-options - */ -export function warn(message: string, options?: { id?: string }): void; - -/** - * Display a deprecation warning with the provided message and a stack trace - * (Chrome and Firefox only). - * - * In a production build, this method is defined as an empty function (NOP). - * Uses of this method in Ember itself are stripped from the ember.prod.js build. - * - * @param message A description of the deprecation. - * @param test If falsy, the deprecation will be displayed. - * @param options The deprecation options. - */ -export function deprecate( - message: string, - test: boolean, - options: { - /** - * A unique id for this deprecation. The id can be used by Ember debugging - * tools to change the behavior (raise, log or silence) for that specific - * deprecation. The id should be namespaced by dots, e.g. - * `"view.helper.select"`. - */ - id: string; - /** - * The version of Ember when this deprecation warning will be removed. - */ - until: string; - /** - * An optional url to the transition guide on the emberjs.com website. - */ - url?: string; - /** - * The library emitting this deprecation - */ - for: string; - /** - * Information about the stage for this deprecation - */ - since: { - /** - * The version this deprecation was added in (but not necessarily activated) - */ - available?: string; - /** - * The version this deprecation was/will-be activated - */ - enabled: string; - }; - } -): void; diff --git a/@types/@ember/object/compat.d.ts b/@types/@ember/object/compat.d.ts deleted file mode 100644 index 1f5d4b3c2c8..00000000000 --- a/@types/@ember/object/compat.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export function dependentKeyCompat(desc: PropertyDescriptor): void; -export function dependentKeyCompat(target: object, key: string, desc: PropertyDescriptor): void; diff --git a/@types/@ember/object/promise-proxy-mixin.d.ts b/@types/@ember/object/promise-proxy-mixin.d.ts deleted file mode 100755 index 6939b4057a8..00000000000 --- a/@types/@ember/object/promise-proxy-mixin.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * A low level mixin making ObjectProxy promise-aware. - */ -interface PromiseProxyMixin extends Promise { - /** - * 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; -} -declare class PromiseProxyMixin extends Promise {} -export default PromiseProxyMixin; diff --git a/@types/@ember/object/proxy.d.ts b/@types/@ember/object/proxy.d.ts deleted file mode 100755 index b687c1c3db0..00000000000 --- a/@types/@ember/object/proxy.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import EmberObject from '@ember/object'; -import { - UnwrapComputedPropertyGetter, - UnwrapComputedPropertyGetters, - UnwrapComputedPropertySetters, -} from '@ember/object/-private/types'; - -/** - * `Ember.ObjectProxy` forwards all properties not defined by the proxy itself - * to a proxied `content` object. - */ -export default class ObjectProxy extends EmberObject { - /** - * The object whose properties will be forwarded. - */ - content: T | null | undefined; - - get(key: K): UnwrapComputedPropertyGetter; - get(key: K): UnwrapComputedPropertyGetter | undefined; - - getProperties(list: K[]): Pick, K>; - getProperties(...list: K[]): Pick, K>; - getProperties(list: K[]): Pick>, K>; - getProperties(...list: K[]): Pick>, K>; - - set( - key: K, - value: UnwrapComputedPropertySetters[K] - ): UnwrapComputedPropertySetters[K]; - set(key: K, value: UnwrapComputedPropertySetters[K]): UnwrapComputedPropertySetters[K]; - - setProperties( - hash: Pick, K> - ): Pick, K>; -} diff --git a/@types/@ember/runloop/-private/backburner.d.ts b/@types/@ember/runloop/-private/backburner.d.ts deleted file mode 100644 index 01c0c352833..00000000000 --- a/@types/@ember/runloop/-private/backburner.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { EmberRunTimer } from '@ember/runloop/types'; - -export interface QueueItem { - method: string; - target: object; - args: object[]; - stack: string | undefined; -} - -export interface DeferredActionQueues { - [index: string]: unknown; - queues: object; - schedule( - queueName: string, - target: unknown, - method: unknown, - args: unknown, - onceFlag: boolean, - stack: unknown - ): unknown; - flush(fromAutorun: boolean): unknown; -} - -export interface DebugInfo { - autorun: Error | undefined | null; - counters: object; - timers: QueueItem[]; - instanceStack: DeferredActionQueues[]; -} - -export interface Backburner { - join(fn: () => T): T; - on(...args: unknown[]): void; - scheduleOnce(...args: unknown[]): EmberRunTimer; - run(fn: () => T): T; - schedule(queueName: string, target: object | null, method: (() => void) | string): EmberRunTimer; - ensureInstance(): void; - DEBUG: boolean; - getDebugInfo(): DebugInfo; -} diff --git a/@types/@ember/runloop/index.d.ts b/@types/@ember/runloop/index.d.ts deleted file mode 100644 index 7d4aef9e129..00000000000 --- a/@types/@ember/runloop/index.d.ts +++ /dev/null @@ -1,283 +0,0 @@ -// Type definitions for non-npm package @ember/runloop 3.16 -// Project: https://emberjs.com/api/ember/3.16/modules/@ember%2Frunloop -// Definitions by: Mike North -// Steve Calvert -// Chris Krycho -// Dan Freeman -// James C. Davis -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 3.7 - -import type { Backburner } from '@ember/runloop/-private/backburner'; -import type { EmberRunQueues, RunMethod } from '@ember/runloop/-private/types'; -import type { EmberRunTimer } from '@ember/runloop/types'; - -export interface RunNamespace { - /** - * Runs the passed target and method inside of a RunLoop, ensuring any - * deferred actions including bindings and views updates are flushed at the - * end. - */ - (method: (...args: unknown[]) => Ret): Ret; - (target: Target, method: RunMethod, ...args: unknown[]): Ret; - /** - * If no run-loop is present, it creates a new one. If a run loop is - * present it will queue itself to run on the existing run-loops action - * queue. - */ - join(method: (...args: unknown[]) => Ret, ...args: unknown[]): Ret | undefined; - join(target: Target, method: RunMethod, ...args: unknown[]): Ret | undefined; - /** - * Allows you to specify which context to call the specified function in while - * adding the execution of that function to the Ember run loop. This ability - * makes this method a great way to asynchronously integrate third-party libraries - * into your Ember application. - */ - bind(target: Target, method: RunMethod, ...args: unknown[]): (...args: unknown[]) => Ret; - /** - * Begins a new RunLoop. Any deferred actions invoked after the begin will - * be buffered until you invoke a matching call to `run.end()`. This is - * a lower-level way to use a RunLoop instead of using `run()`. - */ - begin(): void; - /** - * Ends a RunLoop. This must be called sometime after you call - * `run.begin()` to flush any deferred actions. This is a lower-level way - * to use a RunLoop instead of using `run()`. - */ - end(): void; - /** - * Adds the passed target/method and any optional arguments to the named - * queue to be executed at the end of the RunLoop. If you have not already - * started a RunLoop when calling this method one will be started for you - * automatically. - */ - schedule(queue: EmberRunQueues, target: Target, method: keyof Target, ...args: unknown[]): EmberRunTimer; - schedule(queue: EmberRunQueues, target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; - schedule(queue: EmberRunQueues, method: (args: unknown[]) => unknown, ...args: unknown[]): EmberRunTimer; - /** - * Invokes the passed target/method and optional arguments after a specified - * period of time. The last parameter of this method must always be a number - * of milliseconds. - */ - later(method: (...args: unknown[]) => unknown, wait: number): EmberRunTimer; - later(target: Target, method: RunMethod, wait: number): EmberRunTimer; - later(target: Target, method: RunMethod, arg0: unknown, wait: number): EmberRunTimer; - later(target: Target, method: RunMethod, arg0: unknown, arg1: unknown, wait: number): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - arg5: unknown, - wait: number - ): EmberRunTimer; - /** - * Schedule a function to run one time during the current RunLoop. This is equivalent - * to calling `scheduleOnce` with the "actions" queue. - */ - once(target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; - /** - * Schedules a function to run one time in a given queue of the current RunLoop. - * Calling this method with the same queue/target/method combination will have - * no effect (past the initial call). - */ - scheduleOnce( - queue: EmberRunQueues, - target: Target, - method: RunMethod, - ...args: unknown[] - ): EmberRunTimer; - /** - * Schedules an item to run from within a separate run loop, after - * control has been returned to the system. This is equivalent to calling - * `run.later` with a wait time of 1ms. - */ - next(target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; - next(method: () => void, ...args: unknown[]): EmberRunTimer; - - /** - * Cancels a scheduled item. Must be a value returned by `run.later()`, - * `run.once()`, `run.scheduleOnce()`, `run.next()`, `run.debounce()`, or - * `run.throttle()`. - */ - cancel(timer: EmberRunTimer): boolean; - /** - * Delay calling the target method until the debounce period has elapsed - * with no additional debounce calls. If `debounce` is called again before - * the specified time has elapsed, the timer is reset and the entire period - * must pass again before the target method is called. - */ - debounce(method: (...args: unknown[]) => unknown, wait: number, immediate?: boolean): EmberRunTimer; - debounce(target: Target, method: RunMethod, wait: number, immediate?: boolean): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - arg5: unknown, - wait: number, - immediate?: boolean - ): EmberRunTimer; - /** - * Ensure that the target method is never called more frequently than - * the specified spacing period. The target method is called immediately. - */ - throttle(method: (...args: unknown[]) => unknown, spacing: number, immediate?: boolean): EmberRunTimer; - throttle(target: Target, method: RunMethod, spacing: number, immediate?: boolean): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: unknown, - arg1: unknown, - arg2: unknown, - arg3: unknown, - arg4: unknown, - arg5: unknown, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - - queues: EmberRunQueues[]; -} - -// necessary because our "run" is run.backburner -// which we use to avoid autorun triggering for Ember <= 3.4 -// we can drop this and use run directly ~11/1/2019 -export const _backburner: Backburner; -export const run: RunNamespace; -export const begin: typeof run.begin; -export const bind: typeof run.bind; -export const cancel: typeof run.cancel; -export const debounce: typeof run.debounce; -export const end: typeof run.end; -export const join: typeof run.join; -export const later: typeof run.later; -export const next: typeof run.next; -export const once: typeof run.once; -export const schedule: typeof run.schedule; -export const scheduleOnce: typeof run.scheduleOnce; -export const throttle: typeof run.throttle; diff --git a/@types/@ember/utils/index.d.ts b/@types/@ember/utils/index.d.ts deleted file mode 100644 index 61934b4ff0d..00000000000 --- a/@types/@ember/utils/index.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36809 -export function typeOf(v: unknown): 'object' | 'undefined'; - -/** - * Compares two javascript values and returns: - */ -export function compare(v: unknown, w: unknown): number; - -/** - * A value is blank if it is empty or a whitespace string. - */ -export function isBlank(obj?: unknown): boolean; - -/** - * Verifies that a value is `null` or an empty string, empty array, - * or empty function. - */ -export function isEmpty(obj?: unknown): boolean; - -/** - * Compares two objects, returning true if they are equal. - */ -export function isEqual(a: unknown, b: unknown): boolean; - -/** - * Returns true if the passed value is null or undefined. This avoids errors - * from JSLint complaining about use of ==, which can be technically - * confusing. - */ -export function isNone(obj?: unknown): obj is null | undefined; - -/** - * A value is present if it not `isBlank`. - */ -export function isPresent(obj?: unknown): boolean; diff --git a/@types/@ember/version.d.ts b/@types/@ember/version.d.ts deleted file mode 100644 index 5ca42bd40c3..00000000000 --- a/@types/@ember/version.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION: string; diff --git a/@types/@glimmer/tracking.d.ts b/@types/@glimmer/tracking.d.ts index b15344d3d05..e434504b074 100644 --- a/@types/@glimmer/tracking.d.ts +++ b/@types/@glimmer/tracking.d.ts @@ -1,3 +1,3 @@ export function cached(target: object, key: string, desc: PropertyDescriptor): void; -export function tracked(target: object, key: string): void; +export function tracked(target: object, key: string, desc?: object): void; diff --git a/@types/ember-data-qunit-asserts/index.d.ts b/@types/ember-data-qunit-asserts/index.d.ts index b71e9c97c0d..7df36522a7d 100644 --- a/@types/ember-data-qunit-asserts/index.d.ts +++ b/@types/ember-data-qunit-asserts/index.d.ts @@ -1,3 +1,7 @@ +import type { CacheOperation, NotificationType } from '@ember-data/store/-private/managers/notification-manager'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; + declare global { interface DeprecationConfig { id: string; @@ -15,24 +19,70 @@ declare global { } interface Assert { - expectDeprecation(options: Partial & { id: string; count: number }): void; - expectDeprecation(callback: () => unknown, options: DeprecationConfig | string | RegExp): Promise; - expectNoDeprecation(callback: () => unknown): Promise; + expectDeprecation(options: DeprecationConfig, label?: string): void; + expectDeprecation( + callback: () => void | Promise, + options: DeprecationConfig | string | RegExp, + label?: string + ): Promise; + expectNoDeprecation( + callback: () => void | Promise, + label?: string, + filter?: (deprecation: FoundDeprecation) => boolean + ): Promise; expectWarning(callback: () => unknown, options: WarningConfig | string | RegExp): Promise; expectNoWarning(callback: () => unknown): Promise; expectAssertion(callback: () => unknown, matcher: string | RegExp): Promise; expectNoAssertion(callback: () => unknown): Promise; + /** + * Asserts that each member of actual strictly matches the corresponding member of expected. + * Asserts that actual is an array and has the same length as expected. + */ + arrayStrictEquals(actual: unknown, expected: T[], message: string): void; + /** + * Asserts that the given identifier has been notified of a change to the given bucket + * and optional key the given number of times during the test. + * + * Clears the notification count for the given identifier, bucket and key after the assertion + * is made so that it is easy to assert notification counts in between steps of a test. + */ + notified( + identifier: StableDocumentIdentifier | StableRecordIdentifier, + bucket: NotificationType | CacheOperation, + key: string | null, + count: number + ): void; + + watchNotifications(store?: unknown): void; + clearNotifications(): void; } namespace QUnit { export interface Assert { expectDeprecation(options: { id: string; count: number; until?: string }): void; - expectDeprecation(callback: () => unknown, options: DeprecationConfig | string | RegExp): Promise; - expectNoDeprecation(callback: () => unknown): Promise; + expectDeprecation( + callback: () => void | Promise, + options: DeprecationConfig | string | RegExp + ): Promise; + expectNoDeprecation( + callback: () => void | Promise, + label?: string, + filter?: (deprecation: FoundDeprecation) => boolean + ): Promise; expectWarning(callback: () => unknown, options: WarningConfig | string | RegExp): Promise; expectNoWarning(callback: () => unknown): Promise; expectAssertion(callback: () => unknown, matcher: string | RegExp): Promise; expectNoAssertion(callback: () => unknown): Promise; + arrayStrictEquals(unknown, expected: T[], message: string): void; + notified( + identifier: StableDocumentIdentifier | StableRecordIdentifier, + bucket: NotificationType | CacheOperation, + key: string | null, + count: number + ): void; + + watchNotifications(store?: unknown): void; + clearNotifications(): void; } } diff --git a/@types/ember/index.d.ts b/@types/ember/index.d.ts deleted file mode 100644 index 19ad6401313..00000000000 --- a/@types/ember/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import EmberArrayProtoExtensions from '@ember/array/types/prototype-extensions'; - -declare module 'ember' { - export function run(callback: Function); - export function meta(obj: Object): { - hasMixin(mixin: Object): boolean; - }; - interface ArrayPrototypeExtensions extends EmberArrayProtoExtensions {} -} diff --git a/@types/require/index.d.ts b/@types/require/index.d.ts deleted file mode 100644 index 67fffb84de9..00000000000 --- a/@types/require/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function (moduleName: string): unknown; - -export function has(moduleName: string): boolean; diff --git a/RELEASE.md b/RELEASE.md index 6f0320f0aac..e1797d9d98f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,245 +1,73 @@ # Release -Although not very tricky, the Ember Data release process does have a -few manual steps. The following steps navigate us through -some of the release gotchas and will hopefully result in a successful -release. +The EmberData release process is mostly automated but requires manually configuring +and triggering the appropriate workflow. -There are four release channels, `lts`, `release`, `beta` and `canary`. -Each has it's own section below. +There are four standard and two non-standard release channels -In this guide, we are assuming that the remote `origin` is `git@github.com:emberjs/data.git`, -this remote needs to exist and `origin/main` `origin/beta` `origin/release` etc. need to be the upstreams of the local `main` `beta` `release` branches etc. +- standard releases: `lts`, `release`, `beta`, `canary`. +- non-standard releases: `lts-prev` `release-prev` + +## Before We Start + +Before we begin the release train, make sure that the [roadmap](./ROADMAP.md) is properly +updated on `main` and `beta` so that it will be accurate when the new release branch is +created. To do this you likely need to reach out to EmberData core team members to ensure +all recent planning discussions and work is properly accounted for. ## Getting Setup To Do A Release -In order to release `ember-data` you must first ensure the following things: +In order to release EmberData you must have commit rights to `ember-data` on GITHUB. +Everything else is handled by automation. + +In the event you do need to perform a manuall release, you must also have permission +to push to protected branches, and access tokens for npm and github with permissions +to the related package scopes. For more information about manual releases run +`bun release about` in the repository. + +For manually releases you will need to ensure at least the following: - You have `commit` rights to `ember-data` on GitHub -- You have an account on `npm` and belongs to the `ember-data` organization on NPM -- You have `publish` rights within the `ember-data` organization on NPM +- You have an account on `npm` and belongs to the `ember-data` and `warp-drive` organizations on NPM +- You have `publish` rights within the `ember-data` and `warp-drive` organizations on NPM - You have configured your NPM account to use `2fa` (two factor authentication) - You have logged into your NPM account on your machine (typically sessions preserve nearly forever once you have) - You have configured `GITHUB_AUTH` token for `lerna-changelog` to be able to gather info for the release notes. -- You have installed `pnpm` and `node` globally (or better, via `volta`) +- You have installed `bun`, `pnpm` and `node` globally (or better, via `volta`) +- the remote `origin` is `git@github.com:emberjs/data.git`, +-`origin/main` `origin/beta` `origin/release` etc. need to be the upstreams of the local `main` `beta` `release` branches etc. ## Release Order -When releasing more than one channel, we release from "most stable" to "least stable" +When releasing more than one channel, we release from "most stable" to "least stable". +This is what allows changes to flow down from canary to lts versioned seamlessly. - `lts` (_Most Stable_) - `release` - `beta` - `canary` (_Least Stable_) -## Announce release! - -Once you have finished this release process, we recommend posting an announcement to -Twitter the Crosslinking the announcement to the following Discord channels. - -- [#news-and-announcements](https://discordapp.com/channels/480462759797063690/480499624663056390) -- [#dev-ember-data](https://discordapp.com/channels/480462759797063690/480501977931972608) -- [#ember-data](https://discordapp.com/channels/480462759797063690/486549196837486592) - -### LTS Release - -1. Checkout the correct branch - - a. For the first release of a new `LTS`, create a new branch from `origin/release` - - DO THIS PRIOR TO PUBLISHING THE NEXT RELEASE - - ``` - git fetch origin; - git checkout -b lts-- origin/release; - ``` - - b. For subsequent releases of this `LTS`, ensure your local branch is in-sync with the remote. - - ``` - git fetch origin; - git checkout lts--; - git reset --hard origin/lts--; - ``` - -2. Generate the Changelog - -> Note: If this is the first release of the LTS and there are no changes, just add an entry for the next patch version stating we are promoting the release to LTS. - -The Changelog is generated with [lerna-changelog](https://github.com/lerna/lerna-changelog). +Since non-standard releases are always bespoke, they do not participate in the above flow. -The changelog is generated based on labels applied to PRs since the last release. These labels are configured in the root `package.json`. Before merging PRs reviewers should always ensure a meaningful title for the changelog exists. +You will find the automated workflows to perform these releases under the actions tab on github. -For the first release of an LTS, `previous-version` will be the last released version of the `release` channel: e.g. `v4.8.1` +## Polish the Release! -For subsequent versions it will be whatever version number we previously published for this LTS. - -To actually generate the changelog, run: - -``` -pnpm exec lerna-changelog --from=PREVIOUS_VERSION_TAG -``` - -Note: if it is the first time that you use lerna-changelog, you might have to add a token to fetch from Github API: -https://github.com/lerna/lerna-changelog#github-token - -Then: - -- insert lerna-changelog output to `CHANGELOG.md` underneath the document title -- commit the changelog and push the change upstream: - -``` -git add CHANGELOG.md; -git commit -m "Update Changelog for v" -git push origin lts-- // Note: alternatively, you can make a PR to lts-- to make sure there are no errors -``` - -3. Publish the LTS - - ``` - node ./scripts/publish.js lts - ``` - -4. Update the Release Notes on Github - -- Visit [Ember Data Releases](https://github.com/emberjs/data/releases) - - Click on the "Tags" - - Click on the tag just published - - Edit the tag, adding a meaningful title and attaching the changelog (see other releases for examples) - - Publish the release! - -### Latest / Stable Release - -1. Checkout the `release` branch and ensure it is in-sync with `origin/release`. - - DO NOT WORK FROM A LOCAL `release` branch THAT DIFFERS - - a. If this is the first `release` release of the cycle, we "cut" from `beta`. - - DO THIS PRIOR TO PUBLISHING THE NEXT BETA - - ``` - git checkout release; - git fetch origin; - git reset --hard origin/beta; - git push origin release -f; - ``` - - b. For subsequent `release` releases during the cycle, we release from the `release` branch. - - ``` - git checkout release; - git fetch origin; - git reset --hard origin/release; - ``` - -2. Generate the Changelog - - IT IS IMPORTANT THAT ALL CHANGES ARE ON THE REMOTE BRANCH SPECIFIED BY HEAD - - `previous-version` will be whatever version we previously published as a `release`. E.g. if our last release was `4.8.4` and now we are publishing `4.9.0` then we would use `--from=v4.8.4` - - ``` - pnpm exec lerna-changelog --from=PREVIOUS_VERSION_TAG - ``` - -- prepend a new section title for this version with Today's date to `CHANGELOG.md` -- insert changelog script output to `CHANGELOG.md` underneath this new section title -- edit changelog output to be as user-friendly as possible (drop [INTERNAL] changes, non-code changes, etc.) -- commit the changelog and push the change upstream - - ``` - git add CHANGELOG.md; - git commit -m "Update Changelog for v"; - git push origin release; - ``` - -5. Publish the release - - ``` - node ./scripts/publish.js release - ``` - -6. Update the Release Notes on Github +First, update the Release Notes on Github - Visit [Ember Data Releases](https://github.com/emberjs/data/releases) - Click on the "more recent tags" - Click on the tag just published - Edit the tag, adding a meaningful title and attaching the changelog (see other releases for examples) - Publish the release! + - Only set the release as latest if it should be the `latest` tag on npm as well (e.g. the `release` channel). LTS/Beta/Canary/LTS-prev/Release-prev should never be marked as `latest`. -### Manual Beta Releases - -> Note: Most Beta Releases should be handled by the `Canary-Mirror-Beta Release` workflow, which should be manually triggered from the actions page. - -1. Checkout the `#beta` branch and ensure it is in-sync with `origin/beta`. - - DO NOT WORK FROM A LOCAL `beta` branch THAT DIFFERS - - a. If this is the first `beta` release of the cycle, we "cut" from `#main`. - - DO THIS PRIOR TO PUBLISHING THE NEXT CANARY - - ``` - git checkout beta; - git fetch origin; - git reset --hard origin/main; - git push origin beta -f; - ``` - - b. For subsequent `beta` releases during the cycle, we release from the beta branch. +Once you have finished this release process, we recommend posting an announcement to your +Threads/Mastadon/Twitter accounts and the crosslinking the announcement to the following +Discord channels. - ``` - git checkout beta; - git fetch origin; - git reset --hard origin/beta; - ``` - -2. Publish the weekly beta - - ``` - node ./scripts/publish.js beta - ``` -### Canary Releases - -1. Checkout the `#main` branch and ensure it is in-sync with `origin/main`. - - DO NOT WORK FROM A LOCAL `main` branch THAT DIFFERS - - ```js - git checkout main; - git fetch origin; - git reset --hard origin/main - ``` - -2. Publish the nightly. - - a. If this is the very first `canary` release for a new minor - - ``` - node ./scripts/publish.js canary --bumpMinor - ``` - - b. If this is the very first `canary` release for a new major - - ``` - node ./scripts/publish.js canary --bumpMajor - ``` - - c. For all other "nightly" canary releases - - ``` - node ./scripts/publish.js canary - ``` - -Congrats, you are finished! - -#### Canary Auto Publish - -New canary versions are published to npm every Tuesday and Friday at 12pm PST by the `Alpha Release` GitHub action. They can also be published using the workflow trigger. +- [#news-and-announcements](https://discordapp.com/channels/480462759797063690/480499624663056390) +- [#dev-ember-data](https://discordapp.com/channels/480462759797063690/480501977931972608) +- [#ember-data](https://discordapp.com/channels/480462759797063690/486549196837486592) -It will always increment the pre-release version of what's currently in the root `package.json`. For example from `3.25.0-alpha.1` to `3.25.0-alpha.2`. **It requires a human to manually bump minor and major versions and publish**. -To try out the script that will be executed in the GitHub action, use: -`node scripts/publish.js canary --dryRun --force --skipSmokeTest`. The `--dryRun` param will skip auto committing the -version change and publishing. diff --git a/config/babel/fix-mjs.cjs b/config/babel/fix-mjs.cjs new file mode 100644 index 00000000000..d059aab1316 --- /dev/null +++ b/config/babel/fix-mjs.cjs @@ -0,0 +1,35 @@ +module.exports = function () { + const addonLocation = process.env.ADDON_LOCATION; + const pkgPath = addonLocation + '/package.json'; + const pkg = require(pkgPath); + const isV1Addon = !pkg['ember-addon'] || pkg['ember-addon'].version === 1; + + function replaceExt(node) { + if (node.value.endsWith('.mjs')) { + node.value = node.value.replace('.mjs', '.js'); + } + if (isV1Addon) { + if (node.value.endsWith('.js')) { + node.value = node.value.replace('.js', ''); + } + } + } + + return { + name: '@warp-drive/internal-config/fix-mjs', + visitor: { + Program(path) { + path.node.body.forEach((node) => { + if (node.type === 'ImportDeclaration' || (node.type === 'ExportNamedDeclaration' && node.source)) { + replaceExt(node.source); + } + }); + }, + CallExpression(path) { + if (path.node.callee.type === 'Import') { + replaceExt(path.node.arguments[0]); + } + }, + }, + }; +}; diff --git a/config/eslint/browser.js b/config/eslint/browser.js new file mode 100644 index 00000000000..5fd77b263d2 --- /dev/null +++ b/config/eslint/browser.js @@ -0,0 +1,54 @@ +// @ts-check +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import * as imports from './imports.js'; +import * as isolation from './isolation.js'; +import * as ts from './typescript.js'; + +// function resolve(name) { +// const fullPath = import.meta.resolve(name); +// if (fullPath.startsWith('file://')) { +// return fullPath.slice(7); +// } +// } + +export function rules(config = {}) { + const ourRules = { + eqeqeq: 'error', + 'new-cap': ['error', { capIsNew: false }], + 'no-caller': 'error', + 'no-cond-assign': ['error', 'except-parens'], + 'no-console': 'error', // no longer recommended in eslint v6, this restores it + 'no-eq-null': 'error', + 'no-eval': 'error', + 'no-unused-vars': ['error', { args: 'none' }], + + // Too many false positives + // See https://github.com/eslint/eslint/issues/11899 and similar + 'require-atomic-updates': 'off', + + 'prefer-rest-params': 'off', + 'prefer-const': 'error', + }; + + return Object.assign( + {}, + js.configs.recommended.rules, + prettier.rules, + imports.rules(), + isolation.rules(config), + ourRules + ); +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function browser(config = {}) { + config.files = Array.isArray(config.files) ? config.files : ['**/*.{js,gjs}']; + const base = ts.browser(config); + // @ts-expect-error + base.languageOptions.parserOptions.project = null; + base.rules = rules(config); + base.plugins = imports.plugins(); + + return base; +} diff --git a/config/eslint/diagnostic.js b/config/eslint/diagnostic.js new file mode 100644 index 00000000000..cb864fbf5d7 --- /dev/null +++ b/config/eslint/diagnostic.js @@ -0,0 +1,23 @@ +import * as isolation from './isolation.js'; +import * as qunit from './qunit.js'; + +const QUNIT_BANNED_IMPORTS = ['ember-qunit', 'qunit', 'ember-exam']; + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function browser(config = {}) { + const base = qunit.ember(config); + base.rules = Object.assign( + base.rules, + { + 'qunit/no-assert-equal': 'off', + }, + isolation.rules({ + allowedImports: ['@ember/test-helpers', '@ember/test-waiters', ...(config.allowedImports ?? [])].filter( + (v) => !QUNIT_BANNED_IMPORTS.includes(v) + ), + }), + config.rules ?? {} + ); + + return base; +} diff --git a/config/eslint/gts.js b/config/eslint/gts.js new file mode 100644 index 00000000000..6ea144af46b --- /dev/null +++ b/config/eslint/gts.js @@ -0,0 +1,9 @@ +import * as ts from './typescript.js'; + +export function browser(config) { + config.files = config.files ?? ['**/*.{gts,gjs}']; + config.enableGlint = true; + const base = ts.browser(config); + + return base; +} diff --git a/config/eslint/ignore.js b/config/eslint/ignore.js new file mode 100644 index 00000000000..2bd95aa5f76 --- /dev/null +++ b/config/eslint/ignore.js @@ -0,0 +1,40 @@ +const RULES = [ + // # unconventional js + 'blueprints/*', + '!./tests/blueprints/*', + 'vendor/*', + + // # Declaration files + '**/*.d.ts', + + // # compiled output + 'dist/*', + 'dist-*/*', + 'addon/*', + 'tmp/*', + 'tmp*/*', + 'DEBUG/*', + 'DEBUG*/*', + '.git/*', + '.broccoli-cache/*', + 'unstable-preview-types/*', + 'vite.config.mjs.timestamp-*', + + // # Special Cases + 'docs/*', + 'coverage/*', + 'node_modules/*', + '.node_modules.ember-try/*', + 'package.json.ember-try', + 'tests/__testfixtures__', +]; + +export function ignoreRules() { + return RULES.slice(); +} + +export function globalIgnores(additionalIgnores) { + return { + ignores: ignoreRules().concat(additionalIgnores ?? []), + }; +} diff --git a/config/eslint/imports.js b/config/eslint/imports.js new file mode 100644 index 00000000000..8653ec46d29 --- /dev/null +++ b/config/eslint/imports.js @@ -0,0 +1,46 @@ +import SimpleImportSortPlugin from 'eslint-plugin-simple-import-sort'; +import ImportPlugin from 'eslint-plugin-import'; + +// See https://github.com/lydell/eslint-plugin-simple-import-sort#custom-grouping +const ImportSortGroups = [ + // Side effect imports. + [`^\u0000`], + // Glimmer & Ember Dependencies + [`^(@ember/|@glimmer|ember$)`], + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + // But not our packages or packages starting with ember- + // eslint-disable-next-line no-useless-escape + [`^(?!@ember\-data)(?!warp\-drive)(?!ember-)(@?\\w)`], + // Packages starting with ember- + // eslint-disable-next-line no-useless-escape + [`^ember\-`], + // Our Packages. + // Things that start with @ember-data + // eslint-disable-next-line no-useless-escape + [`^(@ember\-data|@warp\-drive)`], + // Absolute imports and other imports such as Vue-style `@/foo`. + // Anything that does not start with a dot. + ['^[^.]'], + // Relative imports. + // Anything that starts with a dot. + // eslint-disable-next-line no-useless-escape + [`^\.`], +]; + +export function rules() { + return { + // Imports + 'import/first': 'error', + 'import/newline-after-import': 'error', + 'import/no-duplicates': 'error', + 'simple-import-sort/imports': ['error', { groups: ImportSortGroups }], + }; +} + +export function plugins() { + return { + 'simple-import-sort': SimpleImportSortPlugin, + import: ImportPlugin, + }; +} diff --git a/config/eslint/isolation.js b/config/eslint/isolation.js new file mode 100644 index 00000000000..4f51fa6def7 --- /dev/null +++ b/config/eslint/isolation.js @@ -0,0 +1,65 @@ +const RESTRICTED_IMPORTS = [ + '@ember/-internals/metal', + '@ember/application', + '@ember/application/namespace', + '@ember/array', + '@ember/array/proxy', + '@ember/component', + '@ember/component/helper', + '@ember/controller', + '@ember/debug', + '@ember/debug/data-adapter', + '@ember/edition-utils', + '@ember/object', + '@ember/object/compat', + '@ember/object/computed', + '@ember/object/evented', + '@ember/object/internals', + '@ember/object/mixin', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/owner', + '@ember/routing', + '@ember/routing/route', + '@ember/runloop', + '@ember/service', + '@ember/string', + '@ember/test-helpers', + '@ember/test-waiters', + '@ember/utils', + '@ember/version', + '@glimmer/component', + '@glimmer/env', + '@glimmer/runtime', + '@glimmer/tracking', + '@glimmer/tracking/primitives/cache', + '@glimmer/validator', + 'ember-inflector', + 'ember-qunit', + 'ember-source', + 'ember-source/types', + 'ember', + 'qunit', + 'testem', +]; +export function rules(options) { + return { + 'no-restricted-imports': [ + 'error', + { + paths: options?.allowedImports + ? RESTRICTED_IMPORTS.filter((path) => { + return !options.allowedImports.includes(path); + }) + : RESTRICTED_IMPORTS, + }, + ], + 'no-restricted-globals': [ + 'error', + { + name: 'QUnit', + message: 'Please use the `qunit` import instead of referencing `QUnit` directly.', + }, + ], + }; +} diff --git a/config/eslint/mocha.js b/config/eslint/mocha.js new file mode 100644 index 00000000000..adbb54cdd78 --- /dev/null +++ b/config/eslint/mocha.js @@ -0,0 +1,16 @@ +import * as node from './node.js'; +import mochaPlugin from 'eslint-plugin-mocha'; + +export function cjs(config = {}) { + config.files = config.files || ['tests/**/*.{js,ts}']; + const base = node.cjs(config); + const recommended = mochaPlugin.configs.flat.recommended; + + base.plugins = Object.assign(base.plugins, recommended.plugins); + base.rules = Object.assign(base.rules, recommended.rules, { + // We use setup to set up beforeEach hooks, etc, which should be OK + 'mocha/no-setup-in-describe': 'off', + }); + + return base; +} diff --git a/config/eslint/node.js b/config/eslint/node.js new file mode 100644 index 00000000000..7bb1b3e50da --- /dev/null +++ b/config/eslint/node.js @@ -0,0 +1,118 @@ +// @ts-check +import nodePlugin from 'eslint-plugin-n'; +import globals from 'globals'; + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function cjs(config) { + const result = { + files: [ + 'addon-main.cjs', + 'addon-main.js', + 'babel.config.cjs', + 'config/ember-try.cjs', + 'config/ember-try.js', + 'config/environment.js', + 'config/targets.js', + 'ember-cli-build.cjs', + 'ember-cli-build.js', + 'eslint.config.cjs', + 'rollup.config.cjs', + 'rollup.config.js', + 'testem.cjs', + 'testem.js', + ], + }; + + if (config?.files) { + result.files.push(...config.files); + } + + const finalConfig = Object.assign({}, nodePlugin.configs['flat/recommended-script'], result); + finalConfig.linterOptions = { + reportUnusedDisableDirectives: 'error', + }; + finalConfig.languageOptions = Object.assign({}, finalConfig.languageOptions, { + /** @type {'commonjs'} */ + sourceType: 'commonjs', + /** @type {2022} */ + ecmaVersion: 2022, + globals: Object.assign({}, globals.node, finalConfig.languageOptions.globals, config?.globals ?? {}), + }); + finalConfig.languageOptions.parserOptions = Object.assign({}, finalConfig.languageOptions.parserOptions, { + ...(config?.parserOptions ?? {}), + }); + + finalConfig.rules = Object.assign( + {}, + finalConfig.rules, + { + 'n/no-missing-import': [ + 'error', + { + // this rule has a bug where if a package has never been published + // is generates a false report that its imports are missing + // it also has a bug where it doesn't properly follow exports in package.json + allowModules: ['@warp-drive/build-config', '@warp-drive/diagnostic'], + }, + ], + }, + config?.rules ?? {} + ); + + return finalConfig; +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function esm(config) { + const result = { + files: [ + 'addon-main.mjs', + 'babel.config.mjs', + 'diagnostic.js', + 'diagnostic.mjs', + 'eslint.config.mjs', + 'vite.config.mjs', + 'holodeck.js', + 'holodeck.mjs', + 'rollup.config.mjs', + 'testem.mjs', + ], + }; + + if (config?.files) { + result.files.push(...config.files); + } + + const finalConfig = Object.assign({}, nodePlugin.configs['flat/recommended-module'], result); + finalConfig.linterOptions = { + reportUnusedDisableDirectives: 'error', + }; + finalConfig.languageOptions = Object.assign({}, finalConfig.languageOptions, { + /** @type {'module'} */ + sourceType: 'module', + /** @type {2022} */ + ecmaVersion: 2022, + globals: Object.assign({}, globals.nodeBuiltin, finalConfig.languageOptions.globals, config?.globals ?? {}), + }); + finalConfig.languageOptions.parserOptions = Object.assign({}, finalConfig.languageOptions.parserOptions, { + ...(config?.parserOptions ?? {}), + }); + + finalConfig.rules = Object.assign( + {}, + finalConfig.rules, + { + 'n/no-missing-import': [ + 'error', + { + // this rule has a bug where if a package has never been published + // is generates a false report that its imports are missing + allowModules: ['@warp-drive/build-config', '@warp-drive/diagnostic'], + }, + ], + }, + config?.rules ?? {} + ); + + return finalConfig; +} diff --git a/config/eslint/parser.js b/config/eslint/parser.js new file mode 100644 index 00000000000..6f6cb484a6a --- /dev/null +++ b/config/eslint/parser.js @@ -0,0 +1,33 @@ +import babelParser from '@babel/eslint-parser'; + +function resolve(name) { + const fullPath = import.meta.resolve(name); + if (fullPath.startsWith('file://')) { + return fullPath.slice(7); + } +} + +export function languageOptions() { + return { + parser: babelParser, + /** @type {2022} */ + ecmaVersion: 2022, + /** @type {'module'} */ + sourceType: 'module', + parserOptions: { + requireConfigFile: false, + babelOptions: { + babelrc: false, + configFile: false, + // eslint-disable-next-line n/no-unpublished-require + plugins: [[resolve('@babel/plugin-proposal-decorators'), { legacy: true }]], + }, + }, + }; +} + +export function defaults() { + return { + languageOptions: languageOptions(), + }; +} diff --git a/config/eslint/qunit.js b/config/eslint/qunit.js new file mode 100644 index 00000000000..f55f5c2fdbb --- /dev/null +++ b/config/eslint/qunit.js @@ -0,0 +1,53 @@ +import * as isolation from './isolation.js'; +import * as typescript from './typescript.js'; +import lintQUnit from 'eslint-plugin-qunit'; + +const QUNIT_IMPORTS = ['@ember/test-helpers', '@ember/test-waiters', 'ember-qunit', 'qunit']; + +export function rules(config = {}) { + const ourRules = { + 'qunit/no-assert-logical-expression': 'off', + 'qunit/no-conditional-assertions': 'off', + 'qunit/no-early-return': 'off', + 'qunit/no-ok-equality': 'off', + 'qunit/require-expect': 'off', + }; + + config.allowedImports = Array.isArray(config.allowedImports) + ? config.allowedImports.concat(QUNIT_IMPORTS) + : QUNIT_IMPORTS.slice(); + + return Object.assign({}, lintQUnit.configs.recommended.rules, isolation.rules(config), ourRules); +} + +export function plugins() { + return { qunit: lintQUnit }; +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function ember(config = {}) { + config.allowedImports = Array.isArray(config.allowedImports) + ? config.allowedImports.concat(QUNIT_IMPORTS) + : QUNIT_IMPORTS.slice(); + + config.files = config.files || ['tests/**/*.{js,ts,gjs,gts}']; + + const base = typescript.browser(config); + return { + // languageOptions: base.languageOptions, + files: config.files, + plugins: plugins(), + rules: rules(config), + }; +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function node(config = {}) { + config.allowedImports = Array.isArray(config.allowedImports) ? config.allowedImports.concat(['qunit']) : ['qunit']; + + return { + files: config.files || ['tests/**/*.{js,ts}'], + plugins: plugins(), + rules: rules(config), + }; +} diff --git a/config/eslint/typescript.js b/config/eslint/typescript.js new file mode 100644 index 00000000000..6e0f8563697 --- /dev/null +++ b/config/eslint/typescript.js @@ -0,0 +1,200 @@ +// @ts-check +import * as js from './browser.js'; +import * as imports from './imports.js'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; +import noop from 'ember-eslint-parser/noop'; +import emberEslintParser from 'ember-eslint-parser'; + +/** @returns {import('eslint').Linter.FlatConfig} */ +function mergeTsConfigs(configArray) { + const merged = { + languageOptions: {}, + rules: {}, + }; + for (const config of configArray) { + merged.languageOptions = config.languageOptions ? config.languageOptions : merged.languageOptions; + merged.plugins = config.plugins ? config.plugins : merged.plugins; + merged.rules = config.rules ? Object.assign(merged.rules, config.rules) : merged.rules; + } + + return merged; +} + +export function rules(config = {}) { + const ourRules = { + '@typescript-eslint/no-base-to-string': 'off', + '@typescript-eslint/no-invalid-void-type': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/unified-signatures': 'off', + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/no-dynamic-delete': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + disallowTypeAnnotations: false, + }, + ], + 'no-loop-func': 'off', + '@typescript-eslint/no-loop-func': 'error', + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-throw-literal': 'off', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-inferrable-types': 'error', + '@typescript-eslint/no-meaningless-void-operator': 'error', + '@typescript-eslint/only-throw-error': 'error', + // Many failures for these; they seem intentional so I don't want to just auto-fix: + // '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + // '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-unsafe-declaration-merging': 'off', + '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/prefer-includes': 'error', + // Auto-fix changes the types in some of these cases, which didn't seem safe: + // '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-reduce-type-parameter': 'error', + '@typescript-eslint/prefer-return-this-type': 'error', + '@typescript-eslint/prefer-ts-expect-error': 'error', + 'prefer-const': 'error', + 'prefer-rest-params': 'off', + }; + + const finalized = {}; + + if (config.recommended || typeof config.recommended !== 'boolean') { + Object.assign(finalized, mergeTsConfigs(tseslint.configs.recommended).rules); + } + if (config.typeChecked || typeof config.recommended !== 'boolean') { + Object.assign(finalized, mergeTsConfigs(tseslint.configs.recommendedTypeChecked).rules); + } + if (config.strict || typeof config.recommended !== 'boolean') { + Object.assign(finalized, mergeTsConfigs(tseslint.configs.strict).rules); + } + + Object.assign(finalized, ourRules, config?.rules ?? {}); + + return finalized; +} + +export function parser(enableGlint = false) { + if (enableGlint) { + return Object.assign( + { + meta: { + name: 'ember-eslint-parser', + version: '*', + }, + }, + emberEslintParser + ); + } + const merged = mergeTsConfigs(tseslint.configs.recommended); + // @ts-expect-error + return merged.languageOptions.parser; +} + +export function plugins() { + return { + '@typescript-eslint': tseslint.plugin, + }; +} + +export function constructFileGlobs(srcDirs, files) { + const globs = []; + + for (const dir of srcDirs) { + const hasSlash = dir.endsWith('/'); + for (const file of files) { + const needsSlash = !hasSlash && !file.startsWith('/'); + globs.push(`${dir}${needsSlash ? '/' : ''}${file}`); + } + } + + return globs; +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function browser(config) { + config.files = config.files ?? ['**/*.ts']; + /** @type {string[]} */ + const files = Array.isArray(config.srcDirs) ? constructFileGlobs(config.srcDirs, config.files) : config.files; + + const lintconfig = { + files, + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + languageOptions: { + parser: parser(config.enableGlint), + parserOptions: { + project: './tsconfig.json', + // projectService: true, + // tsconfigRootDir: import.meta.dirname, + extraFileExtensions: ['.gts', '.gjs'], + }, + /** @type {2022} */ + ecmaVersion: 2022, + /** @type {'module'} */ + sourceType: 'module', + globals: Object.assign({}, globals.browser, config.globals), + }, + rules: Object.assign({}, js.rules(config), rules(config)), + // @ts-expect-error + plugins: Object.assign({}, imports.plugins(), plugins()), + }; + + if (config.enableGlint) { + lintconfig.processor = 'ember/noop'; + lintconfig.plugins = Object.assign({}, lintconfig.plugins, { + ember: { + meta: { + name: 'ember', + version: '*', + }, + processors: { + noop, + }, + }, + }); + } + + return lintconfig; +} + +/** @returns {import('eslint').Linter.FlatConfig} */ +export function node(config) { + config.files = config.files ?? ['**/*.ts']; + /** @type {string[]} */ + const files = Array.isArray(config.srcDirs) ? constructFileGlobs(config.srcDirs, config.files) : config.files; + + return { + files, + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + languageOptions: { + parser: parser(), + parserOptions: { + project: './tsconfig.json', + extraFileExtensions: ['.gts', '.gjs'], + }, + /** @type {2022} */ + ecmaVersion: 2022, + /** @type {'module'} */ + sourceType: 'module', + globals: Object.assign({}, globals.node, config.globals), + }, + rules: Object.assign({}, js.rules(config), rules(config)), + // @ts-expect-error + plugins: Object.assign({}, imports.plugins(), plugins()), + }; +} diff --git a/config/package.json b/config/package.json new file mode 100644 index 00000000000..38efe8f7314 --- /dev/null +++ b/config/package.json @@ -0,0 +1,37 @@ +{ + "name": "@warp-drive/internal-config", + "private": true, + "version": "4.12.8", + "type": "module", + "dependencies": { + "@babel/cli": "^7.24.5", + "@babel/core": "^7.24.5", + "@babel/eslint-parser": "7.25.8", + "@rollup/plugin-babel": "^6.0.4", + "@typescript-eslint/eslint-plugin": "^8.10.0", + "@typescript-eslint/parser": "^8.10.0", + "typescript-eslint": "^8.10.0", + "@embroider/addon-dev": "^4.3.1", + "@eslint/js": "^9.13.0", + "globals": "^15.11.0", + "ember-eslint-parser": "^0.5.2", + "eslint": "^9.12.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-mocha": "^10.5.0", + "eslint-plugin-n": "^17.11.0", + "eslint-plugin-qunit": "^8.1.2", + "eslint-plugin-simple-import-sort": "^12.1.1", + "rollup": "^4.17.2", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vite-plugin-dts": "^3.9.1" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../package.json" + }, + "packageManager": "pnpm@8.15.9" +} diff --git a/config/rollup/external.js b/config/rollup/external.js new file mode 100644 index 00000000000..f65507dd201 --- /dev/null +++ b/config/rollup/external.js @@ -0,0 +1,93 @@ +import path from 'path'; +import fs from 'fs'; +import { globSync } from 'fs'; + +function loadConfig() { + const configPath = path.join(process.cwd(), './package.json'); + const pkg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + return pkg; +} + +export function entryPoints(globs, resolve, options) { + const files = []; + + // expand all globs + globs.forEach((glob) => { + glob.includes('*') || glob.includes('{') ? files.push(...globSync(glob)) : files.push(glob); + }); + + const srcDir = resolve(options.srcDir.startsWith('.') ? options.srcDir : './' + options.srcDir).slice(7) + '/'; + + // resolve all files to full paths + const allFiles = files.map((v) => { + if (!v.startsWith('.')) { + v = './' + v; + } + + const file = resolve(v); + if (file.startsWith('file://')) { + return file.slice(7); + } + return file; + }); + + const fileMap = {}; + allFiles.forEach((file) => { + let name; + if (options.flatten) { + // extract the file name sans directory and extension + name = path.basename(file, path.extname(file)); + } else { + // extract the file name sans srcDir directory and extension + name = file.replace(srcDir, ''); + name = name.slice(0, name.length - path.extname(name).length); + } + fileMap[name] = file; + }); + // console.log({ srcDir, fileMap }); + return fileMap; +} + +export function external(manual = []) { + const pkg = loadConfig(); + const deps = Object.keys(pkg.dependencies || {}); + const peers = Object.keys(pkg.peerDependencies || {}); + const all = new Set([...deps, ...peers, ...manual]); + + // console.log({ externals: result }); + return function (id) { + if (all.has(id)) { + return true; + } + + for (const dep of deps) { + if (id.startsWith(dep + '/')) { + return true; + } + } + + for (const dep of peers) { + if (id.startsWith(dep + '/')) { + return true; + } + } + + if (id.startsWith('@warp-drive/build-config/') && pkg.devDependencies?.['@warp-drive/build-config']) { + return true; + } + + if (id.startsWith('@embroider/macros') && pkg.devDependencies?.['@embroider/macros']) { + return true; + } + + if (id.startsWith('expect-type') && pkg.devDependencies?.['expect-type']) { + return true; + } + + if (id.startsWith('@ember/') || id.startsWith('@ember-data/') || id.startsWith('@warp-drive/')) { + throw new Error(`Unexpected import: '${id}' is neither a dependency nor a peerDependency.`); + } + + return false; + }; +} diff --git a/config/rollup/gjs.js b/config/rollup/gjs.js new file mode 100644 index 00000000000..0244310a5e9 --- /dev/null +++ b/config/rollup/gjs.js @@ -0,0 +1,3 @@ +import { Addon } from '@embroider/addon-dev/rollup'; + +export const gjs = Addon.prototype.gjs; diff --git a/config/vite/compile-types-plugin.js b/config/vite/compile-types-plugin.js new file mode 100644 index 00000000000..ea8af950e66 --- /dev/null +++ b/config/vite/compile-types-plugin.js @@ -0,0 +1,11 @@ +import child_process from 'child_process'; + +export function CompileTypesPlugin(useGlint) { + return { + name: 'compile-types-with-tsc', + + closeBundle: () => { + child_process.spawnSync(useGlint ? 'glint' : 'tsc', ['--build'], { stdio: 'inherit' }); + }, + }; +} diff --git a/config/vite/config.js b/config/vite/config.js new file mode 100644 index 00000000000..ce0f3c2fb69 --- /dev/null +++ b/config/vite/config.js @@ -0,0 +1,57 @@ +import { babel } from '@rollup/plugin-babel'; +import { external, entryPoints } from '../rollup/external.js'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { FixModuleOutputPlugin } from './fix-module-output-plugin.js'; +// import { CompileTypesPlugin } from './compile-types-plugin.js'; + +export function createConfig(options, resolve) { + options.srcDir = options.srcDir ?? './src'; + options.fixModule = options.fixModule ?? true; + options.rollupTypes = options.rollupTypes ?? false; + options.compileTypes = options.compileTypes ?? true; + + return defineConfig({ + esbuild: false, + logLevel: 'error', + reportCompressedSize: false, + build: { + outDir: 'dist', + emptyOutDir: options.emptyOutDir ?? true, + target: options.target ?? ['esnext', 'firefox121'], + minify: false, + sourcemap: true, + lib: { + entry: entryPoints(options.entryPoints, resolve, options), + formats: options.format ? [options.format] : ['es'], + }, + rollupOptions: { + external: external(options.externals), + plugins: options.rollup?.plugins, + output: { + hoistTransitiveImports: false, + preserveModules: options.rollup?.preserveModules ?? false, + }, + }, + }, + plugins: [ + babel({ + babelHelpers: 'bundled', + extensions: ['.js', '.ts', '.gjs', '.gts'], + }), + options.compileTypes === true && options.rollupTypes === true + ? dts({ + rollupTypes: true, + outDir: 'unstable-preview-types', + logLevel: 'silent', + afterDiagnostic: (diagnostic) => {}, + }) + : undefined, + options.fixModule ? FixModuleOutputPlugin : undefined, + // options.compileTypes === true && options.rollupTypes === false ? CompileTypesPlugin(options.useGlint) : undefined, + ...(options.plugins ?? []), + ] + .concat(options.plugins || []) + .filter(Boolean), + }); +} diff --git a/config/vite/fix-module-output-plugin.js b/config/vite/fix-module-output-plugin.js new file mode 100644 index 00000000000..9077cd47287 --- /dev/null +++ b/config/vite/fix-module-output-plugin.js @@ -0,0 +1,59 @@ +import child_process from 'child_process'; +import { globSync, readFileSync, writeFileSync } from 'fs'; +import path from 'path'; + +const DEBUG = process.env.DEBUG === '*'; + +export const FixModuleOutputPlugin = { + name: 'use-weird-non-ESM-ember-convention', + + closeBundle: () => { + /** + * Related issues + * - https://github.com/embroider-build/embroider/issues/1672 + * - https://github.com/embroider-build/embroider/pull/1572 + * - https://github.com/embroider-build/embroider/issues/1675 + * + * Fixed in embroider@4 and especially @embroider/vite + */ + const files = globSync('./dist/**/*.mjs'); + if (files.length === 0) { + DEBUG && console.log('🟡 No MJS files found to rename to JS'); + return; + } + + for (const file of files) { + child_process.spawnSync('mv', [file, file.replace(/\.mjs$/, '.js')], { stdio: 'inherit' }); + DEBUG && console.log(`\t⚠️ Renamed MJS module ${file} to JS in a CJS package`); + } + + // babel ./dist --out-dir dist --plugins=../../config/babel/fix-mjs.js + const distDir = path.join(process.cwd(), 'dist'); + const babelPlugin = path.join(import.meta.dirname, '../babel/fix-mjs.cjs'); + const args = ['exec', 'babel', distDir, '--out-dir', distDir, '--plugins', babelPlugin]; + child_process.spawnSync('pnpm', args, { + stdio: 'inherit', + cwd: import.meta.dirname, + env: { ...process.env, ADDON_LOCATION: process.cwd() }, + }); + DEBUG && console.log(`\t⚠️ Fixes ${files.length} files to import/export from .js instead of .mjs`); + + const mapFiles = globSync('./dist/**/*.mjs.map'); + if (mapFiles.length === 0) { + DEBUG && console.log('🟡 No MJS map files found to rename to JS'); + return; + } + + for (const file of mapFiles) { + // replace any .mjs references in the map files to .js + const map = path.join(process.cwd(), file); + const mapContent = readFileSync(map, { encoding: 'utf-8' }); + const newContent = mapContent.replaceAll('.mjs', '.js'); + writeFileSync(map, newContent, { encoding: 'utf-8' }); + + // rename the map files + child_process.spawnSync('mv', [file, file.replace(/\.mjs.map$/, '.js.map')], { stdio: 'inherit' }); + DEBUG && console.log(`\t⚠️ Renamed MJS map ${file} to JS in a CJS package`); + } + }, +}; diff --git a/config/vite/keep-assets.js b/config/vite/keep-assets.js new file mode 100644 index 00000000000..c002c6c169c --- /dev/null +++ b/config/vite/keep-assets.js @@ -0,0 +1,21 @@ +import { join } from 'path'; +import { copyFileSync, globSync, mkdirSync } from 'fs'; + +export function keepAssets({ from, include, dist }) { + return { + name: 'copy-assets', + + // the assets go into the output directory in the same relative locations as + // in the input directory + async closeBundle() { + const files = globSync(include, { cwd: join(process.cwd(), from) }); + for (let name of files) { + const fromPath = join(process.cwd(), from, name); + const toPath = join(process.cwd(), dist, name); + + mkdirSync(join(toPath, '..'), { recursive: true }); + copyFileSync(fromPath, toPath); + } + }, + }; +} diff --git a/docs-generator/compile-docs.js b/docs-generator/compile-docs.js index 838b55143ec..cf92743a17b 100644 --- a/docs-generator/compile-docs.js +++ b/docs-generator/compile-docs.js @@ -4,6 +4,45 @@ const path = require('path'); const Y = require('yuidocjs'); const getVersion = require('git-repo-version'); +/** + * This is a fix for decorator handling in code comment blocks that is + * part of https://github.com/cibernox/ember-cli-yuidoc and is what allows + * ember.js yui doc generation to handle decorator syntax. See: + * https://github.com/cibernox/ember-cli-yuidoc/blob/master/lib/broccoli-yuidoc.js + */ +const originalHandleComment = Y.DocParser.prototype.handlecomment; +const AT_PLACEHOLDER = '---AT-PLACEHOLDER---'; +const AT_PLACEHOLDER_REGEX = new RegExp(AT_PLACEHOLDER, 'g'); + +Y.DocParser.prototype.handlecomment = function (comment, file, line) { + const lines = comment.split(/\r\n|\n/); + + let inMarkdownBlock = false; + + const newLines = lines.map((line) => { + if (line.match(/^(\s*\*)?\s*```/)) { + inMarkdownBlock = !inMarkdownBlock; + } + + return inMarkdownBlock ? line.replace(/@/g, AT_PLACEHOLDER) : line; + }); + + const ret = originalHandleComment.call(this, newLines.join('\n'), file, line); + const description = ret.find((t) => t.tag === 'description'); + + if (description) { + description.value = description.value.replace(AT_PLACEHOLDER_REGEX, '@'); + } + + ret + .filter((t) => t.tag === 'example') + .map((example) => { + example.value = example.value.replace(AT_PLACEHOLDER_REGEX, '@'); + }); + + return ret; +}; + function loadYuidocOptions() { return JSON.parse(fs.readFileSync(path.join(__dirname, './yuidoc.json'))); } diff --git a/docs-generator/yui-docs-preprocessor.js b/docs-generator/yui-docs-preprocessor.js index 3c9379a5880..4d69baee43f 100644 --- a/docs-generator/yui-docs-preprocessor.js +++ b/docs-generator/yui-docs-preprocessor.js @@ -2,7 +2,29 @@ function hasProp(obj, prop) { return Object.hasOwnProperty.call(obj, prop); } + +function fixTags(item) { + if (item.description) { + const lines = item.description.split('\n'); + let lastIndex = lines.length; + while (lastIndex-- > 0) { + let value = lines[lastIndex].trim(); + if (value.startsWith('@')) { + let [tag, v] = value.split(' '); + tag = tag.substring(1); + if (!item[tag]) { + item[tag] = v || ''; + } + } else { + break; + } + } + } +} + function shouldKeepItem(item, excludedTags) { + fixTags(item); + for (let i = 0; i < excludedTags.length; i++) { if (hasProp(item, excludedTags[i])) return false; } @@ -11,14 +33,14 @@ function shouldKeepItem(item, excludedTags) { } module.exports = function (data, options) { - console.log('Running ember-data preprocessor...'); + console.log(`Running ember-data preprocessor.`); if (!options.excludeTags) { console.log('no tags to exclude, exiting'); return; } const excludedTags = [...options.excludeTags]; - console.log('Skipping items with the tags: ' + excludedTags); + console.log('Filtering items with the tags: ' + excludedTags.join(`, `)); let acceptedWarnings = []; diff --git a/docs-generator/yuidoc.json b/docs-generator/yuidoc.json index 06b19aba864..77f0bb8dad2 100644 --- a/docs-generator/yuidoc.json +++ b/docs-generator/yuidoc.json @@ -6,18 +6,18 @@ "enabledEnvironments": [], "extension": ".js,.ts", "preprocessor": "./yui-docs-preprocessor.js", - "excludeTags": [ - "internal", - "feature" - ], + "excludeTags": ["internal", "feature", "typedoc", "see", "link", "inheritdoc"], "paths": [ - "../ember-data-types", - "../packages/-ember-data/addon", - "../packages/debug/addon", - "../packages/private-build-infra/virtual-packages", + "../packages/-ember-data/src", + "../packages/core-types/src", + "../packages/debug/src", + "../packages/build-config/src", "../packages/adapter/src", "../packages/model/src", "../packages/serializer/src", + "../packages/rest/src", + "../packages/active-record/src", + "../packages/request-utils/src", "../packages/store/src", "../packages/json-api/src", "../packages/graph/src", diff --git a/ember-data-types/cache/cache.ts b/ember-data-types/cache/cache.ts deleted file mode 100644 index f87ca1c66cc..00000000000 --- a/ember-data-types/cache/cache.ts +++ /dev/null @@ -1,446 +0,0 @@ -/** - * @module @ember-data/experimental-preview-types - */ -import { StoreRequestContext } from '@ember-data/store/-private/cache-handler'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { CollectionResourceRelationship, SingleResourceRelationship } from '../q/ember-data-json-api'; -import { JsonApiError } from '../q/record-data-json-api'; -import { ResourceBlob } from './aliases'; -import { Change } from './change'; -import { ResourceDocument, SingleResourceDataDocument, StructuredDataDocument, StructuredDocument } from './document'; -import { StableDocumentIdentifier } from './identifier'; -import { Mutation } from './mutations'; -import { Operation } from './operations'; - -/** - * The interface for EmberData Caches. - * - * A Cache handles in-memory storage of Document and Resource - * data. - * - * @class Cache - * @public - */ -export interface Cache { - /** - * The Cache Version that this implementation implements. - * - * @type {'2'} - * @public - * @property version - */ - version: '2'; - - // Cache Management - // ================ - - /** - * Cache the response to a request - * - * Unlike `store.push` which has UPSERT - * semantics, `put` has `replace` semantics similar to - * the `http` method `PUT` - * - * the individually cacheable resource data it may contain - * should upsert, but the document data surrounding it should - * fully replace any existing information - * - * Note that in order to support inserting arbitrary data - * to the cache that did not originate from a request `put` - * should expect to sometimes encounter a document with only - * a `content` member and therefor must not assume the existence - * of `request` and `response` on the document. - * - * @method put - * @param {StructuredDocument} doc - * @returns {ResourceDocument} - * @public - */ - put(doc: StructuredDocument): ResourceDocument; - - /** - * Update the "remote" or "canonical" (persisted) state of the Cache - * by merging new information into the existing state. - * - * Note: currently the only valid resource operation is a MergeOperation - * which occurs when a collision of identifiers is detected. - * - * @method patch - * @public - * @param {Operation} op the operation to perform - * @returns {void} - */ - patch(op: Operation): void; - - /** - * Update the "local" or "current" (unpersisted) state of the Cache - * - * @method mutate - * @param {Mutation} mutation - * @returns {void} - * @public - */ - mutate(mutation: Mutation): void; - - /** - * Peek resource data from the Cache. - * - * In development, if the return value - * is JSON the return value - * will be deep-cloned and deep-frozen - * to prevent mutation thereby enforcing cache - * Immutability. - * - * This form of peek is useful for implementations - * that want to feed raw-data from cache to the UI - * or which want to interact with a blob of data - * directly from the presentation cache. - * - * An implementation might want to do this because - * de-referencing records which read from their own - * blob is generally safer because the record does - * not require retainining connections to the Store - * and Cache to present data on a per-field basis. - * - * This generally takes the place of `getAttr` as - * an API and may even take the place of `getRelationship` - * depending on implementation specifics, though this - * latter usage is less recommended due to the advantages - * of the Graph handling necessary entanglements and - * notifications for relational data. - * - * @method peek - * @public - * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier - * @returns {ResourceDocument | ResourceBlob | null} the known resource data - */ - peek(identifier: StableRecordIdentifier): ResourceBlob | null; - peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - - /** - * Peek the Cache for the existing request data associated with - * a cacheable request - * - * @method peekRequest - * @param {StableDocumentIdentifier} - * @returns {StableDocumentIdentifier | null} - * @public - */ - peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; - - /** - * Push resource data from a remote source into the cache for this identifier - * - * @method upsert - * @public - * @param identifier - * @param data - * @param hasRecord - * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned - */ - upsert(identifier: StableRecordIdentifier, data: ResourceBlob, hasRecord: boolean): void | string[]; - - // Cache Forking Support - // ===================== - - /** - * Create a fork of the cache from the current state. - * - * Applications should typically not call this method themselves, - * preferring instead to fork at the Store level, which will - * utilize this method to fork the cache. - * - * @method fork - * @public - * @returns Promise - */ - fork(): Promise; - - /** - * Merge a fork back into a parent Cache. - * - * Applications should typically not call this method themselves, - * preferring instead to merge at the Store level, which will - * utilize this method to merge the caches. - * - * @method merge - * @param {Cache} cache - * @public - * @returns Promise - */ - merge(cache: Cache): Promise; - - /** - * Generate the list of changes applied to all - * record in the store. - * - * Each individual resource or document that has - * been mutated should be described as an individual - * `Change` entry in the returned array. - * - * A `Change` is described by an object containing up to - * three properties: (1) the `identifier` of the entity that - * changed; (2) the `op` code of that change being one of - * `upsert` or `remove`, and if the op is `upsert` a `patch` - * containing the data to merge into the cache for the given - * entity. - * - * This `patch` is opaque to the Store but should be understood - * by the Cache and may expect to be utilized by an Adapter - * when generating data during a `save` operation. - * - * It is generally recommended that the `patch` contain only - * the updated state, ignoring fields that are unchanged - * - * ```ts - * interface Change { - * identifier: StableRecordIdentifier | StableDocumentIdentifier; - * op: 'upsert' | 'remove'; - * patch?: unknown; - * } - * ``` - * - * @method diff - * @public - */ - diff(): Promise; - - // SSR Support - // =========== - - /** - * Serialize the entire contents of the Cache into a Stream - * which may be fed back into a new instance of the same Cache - * via `cache.hydrate`. - * - * @method dump - * @returns {Promise} - * @public - */ - dump(): Promise>; - - /** - * hydrate a Cache from a Stream with content previously serialized - * from another instance of the same Cache, resolving when hydration - * is complete. - * - * This method should expect to be called both in the context of restoring - * the Cache during application rehydration after SSR **AND** at unknown - * times during the lifetime of an already booted application when it is - * desired to bulk-load additional information into the cache. This latter - * behavior supports optimizing pre/fetching of data for route transitions - * via data-only SSR modes. - * - * @method hydrate - * @param {ReadableStream} stream - * @returns {Promise} - * @public - */ - hydrate(stream: ReadableStream): Promise; - - // Resource Support - // ================ - - /** - * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client - * - * It returns properties from options that should be set on the record during the create - * process. This return value behavior is deprecated. - * - * @method clientDidCreate - * @public - * @param identifier - * @param createArgs - */ - clientDidCreate(identifier: StableRecordIdentifier, createArgs?: Record): Record; - - /** - * [LIFECYCLE] Signals to the cache that a resource - * will be part of a save transaction. - * - * @method willCommit - * @public - * @param identifier - */ - willCommit(identifier: StableRecordIdentifier, context: StoreRequestContext): void; - - /** - * [LIFECYCLE] Signals to the cache that a resource - * was successfully updated as part of a save transaction. - * - * @method didCommit - * @public - * @param identifier - the primary identifier that was operated on - * @param data - a document in the cache format containing any updated data - * @return {SingleResourceDataDocument} - */ - didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument; - - /** - * [LIFECYCLE] Signals to the cache that a resource - * was update via a save transaction failed. - * - * @method commitWasRejected - * @public - * @param identifier - * @param errors - */ - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[]): void; - - /** - * [LIFECYCLE] Signals to the cache that all data for a resource - * should be cleared. - * - * This method is a candidate to become a mutation - * - * @method unloadRecord - * @public - * @param identifier - */ - unloadRecord(identifier: StableRecordIdentifier): void; - - // Granular Resource Data APIs - // =========================== - - /** - * Retrieve the data for an attribute from the cache - * - * @method getAttr - * @public - * @param identifier - * @param field - * @returns {unknown} - */ - getAttr(identifier: StableRecordIdentifier, field: string): unknown; - - /** - * Mutate the data for an attribute in the cache - * - * This method is a candidate to become a mutation - * - * @method setAttr - * @public - * @param identifier - * @param field - * @param value - */ - setAttr(identifier: StableRecordIdentifier, field: string, value: unknown): void; - - /** - * Query the cache for the changed attributes of a resource. - * - * @method changedAttrs - * @public - * @deprecated - * @param identifier - * @returns { : [, ] } - */ - changedAttrs(identifier: StableRecordIdentifier): Record; - - /** - * Query the cache for whether any mutated attributes exist - * - * @method hasChangedAttrs - * @public - * @param identifier - * @returns {boolean} - */ - hasChangedAttrs(identifier: StableRecordIdentifier): boolean; - - /** - * Tell the cache to discard any uncommitted mutations to attributes - * - * This method is a candidate to become a mutation - * - * @method rollbackAttrs - * @public - * @param identifier - * @returns {string[]} the names of fields that were restored - */ - rollbackAttrs(identifier: StableRecordIdentifier): string[]; - - /** - * Query the cache for the current state of a relationship property - * - * @method getRelationship - * @public - * @param identifier - * @param field - * @returns resource relationship object - */ - getRelationship( - identifier: StableRecordIdentifier, - field: string, - isCollection?: boolean - ): SingleResourceRelationship | CollectionResourceRelationship; - - // Resource State - // =============== - - /** - * Update the cache state for the given resource to be marked - * as locally deleted, or remove such a mark. - * - * This method is a candidate to become a mutation - * - * @method setIsDeleted - * @public - * @param identifier - * @param isDeleted {boolean} - */ - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void; - - /** - * Query the cache for any validation errors applicable to the given resource. - * - * @method getErrors - * @public - * @param identifier - * @returns {JsonApiError[]} - */ - getErrors(identifier: StableRecordIdentifier): JsonApiError[]; - - /** - * Query the cache for whether a given resource has any available data - * - * @method isEmpty - * @public - * @param identifier - * @returns {boolean} - */ - isEmpty(identifier: StableRecordIdentifier): boolean; - - /** - * Query the cache for whether a given resource was created locally and not - * yet persisted. - * - * @method isNew - * @public - * @param identifier - * @returns {boolean} - */ - isNew(identifier: StableRecordIdentifier): boolean; - - /** - * Query the cache for whether a given resource is marked as deleted (but not - * necessarily persisted yet). - * - * @method isDeleted - * @public - * @param identifier - * @returns {boolean} - */ - isDeleted(identifier: StableRecordIdentifier): boolean; - - /** - * Query the cache for whether a given resource has been deleted and that deletion - * has also been persisted. - * - * @method isDeletionCommitted - * @public - * @param identifier - * @returns {boolean} - */ - isDeletionCommitted(identifier: StableRecordIdentifier): boolean; -} diff --git a/ember-data-types/cache/change.ts b/ember-data-types/cache/change.ts deleted file mode 100644 index 3f51c4f2d63..00000000000 --- a/ember-data-types/cache/change.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { StableDocumentIdentifier } from './identifier'; - -export interface Change { - identifier: StableRecordIdentifier | StableDocumentIdentifier; - op: 'upsert' | 'remove'; - patch?: unknown; -} diff --git a/ember-data-types/cache/document.ts b/ember-data-types/cache/document.ts deleted file mode 100644 index 000601734f6..00000000000 --- a/ember-data-types/cache/document.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { ImmutableRequestInfo, ResponseInfo as ImmutableResponseInfo } from '@ember-data/request/-private/types'; -import { Links, Meta, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; -import { StableExistingRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { JsonApiError } from '../q/record-data-json-api'; - -export type RequestInfo = ImmutableRequestInfo; -export type ResponseInfo = ImmutableResponseInfo; - -export interface ResourceMetaDocument { - // the url or cache-key associated with the structured document - lid?: string; - meta: Meta; - links?: Links | PaginationLinks; -} - -export interface SingleResourceDataDocument { - // the url or cache-key associated with the structured document - lid?: string; - links?: Links | PaginationLinks; - meta?: Meta; - data: T | null; -} - -export interface CollectionResourceDataDocument { - // the url or cache-key associated with the structured document - lid?: string; - links?: Links | PaginationLinks; - meta?: Meta; - data: T[]; -} - -export type ResourceDataDocument = - | SingleResourceDataDocument - | CollectionResourceDataDocument; - -export interface ResourceErrorDocument { - // the url or cache-key associated with the structured document - lid?: string; - links?: Links | PaginationLinks; - meta?: Meta; - errors: JsonApiError[]; -} - -export type ResourceDocument = - | ResourceMetaDocument - | SingleResourceDataDocument - | CollectionResourceDataDocument - | ResourceErrorDocument; - -export interface StructuredDataDocument { - request?: RequestInfo; - response?: ResponseInfo | Response | null; - content: T; -} -export interface StructuredErrorDocument extends Error { - request?: RequestInfo; - response?: ResponseInfo | Response | null; - error: string | object; - content?: T; -} -export type StructuredDocument = StructuredDataDocument | StructuredErrorDocument; diff --git a/ember-data-types/cache/identifier.ts b/ember-data-types/cache/identifier.ts deleted file mode 100644 index 4a3fe7836b3..00000000000 --- a/ember-data-types/cache/identifier.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type StableDocumentIdentifier = { - lid: string; -}; diff --git a/ember-data-types/cache/relationship.ts b/ember-data-types/cache/relationship.ts deleted file mode 100644 index d6a4581fdcc..00000000000 --- a/ember-data-types/cache/relationship.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Value as JSONValue } from 'json-typescript'; - -import { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -// we request that it be in the stable form already. -export interface ResourceRelationship { - data?: StableRecordIdentifier | null; - meta?: Record; - links?: Links; -} - -// Note: in v1 data could be a ResourceIdentifier, now -// we request that it be in the stable form already. -export interface CollectionRelationship { - data?: StableRecordIdentifier[]; - meta?: Record; - links?: PaginationLinks; -} - -export type Relationship = ResourceRelationship | CollectionRelationship; diff --git a/ember-data-types/q/cache-store-wrapper.ts b/ember-data-types/q/cache-store-wrapper.ts deleted file mode 100644 index c76b8549d96..00000000000 --- a/ember-data-types/q/cache-store-wrapper.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache'; -import { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; - -import { StableDocumentIdentifier } from '../cache/identifier'; -import type { Cache } from './cache'; -import { StableRecordIdentifier } from './identifier'; -import type { AttributesSchema, RelationshipsSchema } from './record-data-schemas'; -import { SchemaService } from './schema-service'; - -/** - @module @ember-data/store -*/ - -/** - * CacheStoreWrapper provides encapsulated API access to the minimal - * subset of the Store's functionality that Cache implementations - * should interact with. It is provided to the Store's `createRecordDataFor` - * and `createCache` hooks. - * - * Cache implementations should not need more than this API provides. - * - * This class cannot be directly instantiated. - * - * @class CacheStoreWrapper - * @public - */ -export interface LegacyCacheStoreWrapper { - /** - * Provides access to the IdentifierCache instance - * for this Store instance. - * - * The IdentifierCache can be used to peek, generate or - * retrieve a stable unique identifier for any resource. - * - * @property {IdentifierCache} identifierCache - * @public - */ - identifierCache: IdentifierCache; - - /** - * Provides access to the SchemaDefinitionService instance - * for this Store instance. - * - * The SchemaDefinitionService can be used to query for - * information about the schema of a resource. - * - * @method getSchemaDefinitionService - * @public - */ - getSchemaDefinitionService(): SchemaService; - - /** - * Proxies to the schema service's `relationshipsDefinitionFor` - * method. - * - * Use `wrapper.getSchemaDefinitionService().relationshipsDefinitionFor()` - * instead. - * - * @method relationshipsDefinitionFor - * @param {string} modelName - * @returns {RelationshipsSchema} - * @public - * @deprecated - */ - relationshipsDefinitionFor(modelName: string): RelationshipsSchema; - - /** - * Proxies to the schema service's `attributesDefinitionFor` - * method. - * - * Use `wrapper.getSchemaDefinitionService().attributesDefinitionFor()` - * instead. - * - * @method attributesDefinitionFor - * @param {string} modelName - * @returns {AttributesSchema} - * @public - * @deprecated - */ - attributesDefinitionFor(modelName: string): AttributesSchema; - - /** - * Update the `id` for the record corresponding to the identifier - * This operation can only be done for records whose `id` is `null`. - * - * @method setRecordId - * @param {StableRecordIdentifier} identifier; - * @param {string} id; - * @public - */ - setRecordId(modelName: string, id: string, clientId: string): void; - setRecordId(identifier: StableRecordIdentifier, id: string): void; - - /** - * Signal to the store that the specified record may be considered fully - * removed from the cache. Generally this means that not only does no - * data exist for the identified resource, no known relationships still - * point to it either. - * - * @method disconnectRecord - * @param {StableRecordIdentifier} identifier - * @public - */ - disconnectRecord(modelName: string, id: string | null, clientId: string): void; - disconnectRecord(modelName: string, id: string, clientId?: string | null): void; - disconnectRecord(modelName: string, id: string | null, clientId?: string | null): void; - disconnectRecord(identifier: StableRecordIdentifier): void; - - /** - * Use hasRecord instead. - * - * @method isRecordInUse - * @param modelName - * @param id - * @param clientId - * @public - * @deprecated - */ - isRecordInUse(modelName: string, id: string | null, clientId: string): boolean; - isRecordInUse(modelName: string, id: string, clientId?: string | null): boolean; - isRecordInUse(modelName: string, id: string | null, clientId?: string | null): boolean; - - /** - * Use this method to determine if the Store has an instantiated record associated - * with an identifier. - * - * @method hasRecord - * @param identifier - * @returns {boolean} - * @public - */ - hasRecord(identifier: StableRecordIdentifier): boolean; - - /** - * Use notifyChange - * - * @method notifyPropertyChange - * @param modelName - * @param id - * @param clientId - * @param key - * @deprecated - * @public - */ - notifyPropertyChange(modelName: string, id: string | null, clientId: string | null, key: string): void; - - /** - * Use notifyChange - * - * @method notifyHasManyChange - * @param modelName - * @param id - * @param clientId - * @param key - * @public - * @deprecated - */ - notifyHasManyChange(modelName: string, id: string | null, clientId: string, key: string): void; - notifyHasManyChange(modelName: string, id: string, clientId: string | null | undefined, key: string): void; - notifyHasManyChange(modelName: string, id: string | null, clientId: string | null | undefined, key: string): void; - - /** - * [DEPRECATED] RecordData has become Cache and Cache is now always - * a singleton. - * - * You may access the Cache via Store.cache. If you are interacting - * with this wrapped from the Cache you are the Cache instance and - * thus do not need to call this anymore. - * - * Used to retrieve the associated RecordData for a given identifier. - * - * To generate a RecordData for a new client-side resource that does not - * yet have an ID and place it in the new state, first create an identifier - * via `identifierCache.createIdentifierForNewRecord` - * - * Then once you have obtained the RecordData instance you should invoke - * `recordData.clientDidCreate` to ensure the cache entry is put into the - * correct "newly created" state. - * - * @method recordDataFor - * @deprecated - * @param {StableRecordIdentifier} identifier - * @return {Cache} the RecordData cache instance associated with the identifier - * @public - */ - recordDataFor(type: string, id: string, lid?: string | null): Cache; - recordDataFor(type: string, id: string | null, lid: string): Cache; - recordDataFor(type: string): Cache; - recordDataFor(type: string, id?: string | null, lid?: string | null): Cache; - recordDataFor(identifier: StableRecordIdentifier): Cache; - - /** - * Use notifyChange - * - * @method notifyBelongsToChange - * @param modelName - * @param id - * @param clientId - * @param key - * @public - * @deprecated - */ - notifyBelongsToChange(modelName: string, id: string | null, clientId: string, key: string): void; - notifyBelongsToChange(modelName: string, id: string, clientId: string | null | undefined, key: string): void; - notifyBelongsToChange(modelName: string, id: string | null, clientId: string | null | undefined, key: string): void; - - /** - * Notify subscribers of the NotificationManager that cache state has changed. - * - * `attributes` and `relationships` do not require a key, but if one is specified it - * is assumed to be the name of the attribute or relationship that has been updated. - * - * No other namespaces currently expect the `key` argument. - * - * @method notifyChange - * @param {StableRecordIdentifier} identifier - * @param {'attributes' | 'relationships' | 'identity' | 'errors' | 'meta' | 'state'} namespace - * @param {string|undefined} key - * @public - */ - notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed'): void; - notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void; - notifyChange( - identifier: StableRecordIdentifier, - namespace: NotificationType | 'added' | 'removed', - key?: string - ): void; - - /** - * Use notifyChange - * - * @method notifyErrorsChange - * @param modelName - * @param id - * @param clientId - * @public - * @deprecated - */ - notifyErrorsChange(modelName: string, id: string | null, clientId: string | null): void; - - /** - * Use notifyChange - * - * @method notifyStateChange - * @param modelName - * @param id - * @param clientId - * @param key - * @public - * @deprecated - */ - notifyStateChange(modelName: string, id: string | null, clientId: string | null, key?: string): void; -} - -export interface V2CacheStoreWrapper { - identifierCache: IdentifierCache; - getSchemaDefinitionService(): SchemaService; - - setRecordId(identifier: StableRecordIdentifier, id: string): void; - - disconnectRecord(identifier: StableRecordIdentifier): void; - - hasRecord(identifier: StableRecordIdentifier): boolean; - - recordDataFor(identifier: StableRecordIdentifier): Cache; - - 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 - ): void; -} - -export type CacheStoreWrapper = LegacyCacheStoreWrapper | V2CacheStoreWrapper; diff --git a/ember-data-types/q/cache.ts b/ember-data-types/q/cache.ts deleted file mode 100644 index 27da1f8ae7c..00000000000 --- a/ember-data-types/q/cache.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Cache } from '../cache/cache'; -import type { CollectionResourceRelationship, SingleResourceRelationship } from './ember-data-json-api'; -import type { RecordIdentifier, StableRecordIdentifier } from './identifier'; -import type { JsonApiError, JsonApiResource } from './record-data-json-api'; -import { Dict } from './utils'; -/** - @module @ember-data/store -*/ - -export type ChangedAttributesHash = Record; - -export interface MergeOperation { - op: 'mergeIdentifiers'; - record: StableRecordIdentifier; // existing - value: StableRecordIdentifier; // new -} - -export interface CacheV1 { - version?: '1'; - - // Cache - // ===== - getResourceIdentifier(): RecordIdentifier | undefined; - - pushData(data: JsonApiResource, calculateChange: true): string[]; - pushData(data: JsonApiResource, calculateChange?: false): void; - pushData(data: JsonApiResource, calculateChange?: boolean): string[] | void; - clientDidCreate(): void; - _initRecordCreateOptions(options?: Dict): { [key: string]: unknown }; - - willCommit(): void; - didCommit(data: JsonApiResource | null): void; - commitWasRejected(recordIdentifier?: RecordIdentifier, errors?: JsonApiError[]): void; - - unloadRecord(): void; - - // Attrs - // ===== - getAttr(key: string): unknown; - setDirtyAttribute(key: string, value: unknown): void; - changedAttributes(): ChangedAttributesHash; - hasChangedAttributes(): boolean; - rollbackAttributes(): string[]; - - // Relationships - // ============= - getBelongsTo(key: string): SingleResourceRelationship; - getHasMany(key: string): CollectionResourceRelationship; - - setDirtyBelongsTo(name: string, recordData: Cache | null): void; - setDirtyHasMany(key: string, recordDatas: Cache[]): void; - addToHasMany(key: string, recordDatas: Cache[], idx?: number): void; - removeFromHasMany(key: string, recordDatas: Cache[]): void; - - // State - // ============= - setIsDeleted(isDeleted: boolean): void; - getErrors(identifier: StableRecordIdentifier): JsonApiError[]; - isEmpty?(identifier: StableRecordIdentifier): boolean; // needs rfc - isNew(identifier: StableRecordIdentifier): boolean; - isDeleted(identifier: StableRecordIdentifier): boolean; - isDeletionCommitted(identifier: StableRecordIdentifier): boolean; -} - -export { Cache }; diff --git a/ember-data-types/q/ds-model.ts b/ember-data-types/q/ds-model.ts deleted file mode 100644 index 80e43f297c9..00000000000 --- a/ember-data-types/q/ds-model.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type EmberObject from '@ember/object'; - -import type { Errors } from '@ember-data/model/-private'; -import type Store from '@ember-data/store'; - -import type { JsonApiError } from './record-data-json-api'; -import type { AttributeSchema, RelationshipSchema, RelationshipsSchema } from './record-data-schemas'; - -// Placeholder until model.js is typed -export interface DSModel extends EmberObject { - constructor: DSModelSchema; - store: Store; - errors: Errors; - toString(): string; - save(): Promise; - eachRelationship(callback: (this: T, key: string, meta: RelationshipSchema) => void, binding?: T): void; - eachAttribute(callback: (this: T, key: string, meta: AttributeSchema) => void, binding?: T): void; - invalidErrorsChanged(errors: JsonApiError[]): void; - rollbackAttributes(): void; - changedAttributes(): Record; - [key: string]: unknown; - isDeleted: boolean; - deleteRecord(): void; - unloadRecord(): void; - _notifyProperties(keys: string[]): void; -} - -// Implemented by both ShimModelClass and DSModel -export interface ModelSchema { - modelName: string; - fields: Map; - attributes: Map; - relationshipsByName: Map; - eachAttribute(callback: (this: T, key: string, attribute: AttributeSchema) => void, binding?: T): void; - eachRelationship(callback: (this: T, key: string, relationship: RelationshipSchema) => void, binding?: T): void; - eachTransformedAttribute(callback: (this: T, key: string, type: string | null) => void, binding?: T): void; - toString(): string; -} - -// This is the static side of DSModel should become DSModel -// once we can type it. -export interface DSModelSchema extends ModelSchema { - isModel: true; - relationshipsObject: RelationshipsSchema; - extend(...mixins: unknown[]): DSModelSchema; - reopenClass(...mixins: unknown[]): void; -} diff --git a/ember-data-types/q/ember-data-json-api.ts b/ember-data-types/q/ember-data-json-api.ts deleted file mode 100644 index 84ec4d678ab..00000000000 --- a/ember-data-types/q/ember-data-json-api.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Value as JSONValue } from 'json-typescript'; - -import type { Dict } from './utils'; - -/** - @module @ember-data/store -*/ - -export type Meta = Dict; -export type LinkObject = { href: string; meta?: Dict }; -export type Link = string | LinkObject; -export interface Links { - related?: Link; - self?: Link; -} -export interface PaginationLinks extends Links { - first?: Link | null; - last?: Link | null; - prev?: Link | null; - next?: Link | null; -} - -/** - * Serves as a reference to a `Resource` but does not contain - * any data itself. - * - * Used to establish relationship linkages between `Resources` and - * to address data that may not be available synchronously. - * - * [JSON:API Spec](https://jsonapi.org/format/#document-resource-identifier-objects) - * @internal - */ -export interface ExistingResourceIdentifierObject { - id: string; - type: string; - - /** - * While not officially part of the `JSON:API` spec, - * `ember-data` allows the use of `lid` as a local - * identifier for a `Resource`. - * - * @recommended It is best to include the lid used when creating - * a new resource if this is the response to a new resource creation, - * also recommended if this resource type uses secondary indexes. - * - * Once a `ResourceIdentifierObject` has been seen by the cache, `lid` - * should always be present. Only when inbound from the an `API` response - * is `lid` considered optional. - * - * [Identifiers RFC](https://github.com/emberjs/rfcs/blob/main/text/0403-ember-data-identifiers.md#ember-data--identifiers) - * @internal - */ - lid?: string; - - /** - * While valid in the `JSON:API` spec, - * `ember-data` ignores `meta` on `ResourceIdentifierObjects` - * - * @ignored this property goes un-utilized and will be lost - * @internal - */ - meta?: Meta; -} - -/** - * Serves as a reference to a resource created on the client - * but not yet persisted. - * - * @internal - */ -export interface NewResourceIdentifierObject { - /** - * Resources newly created on the client _may_ - * not have an `id` available to them prior - * to completion of their first successful `save`. - * - * `id` will be `null` in this case. - * - * @internal - */ - id: string | null; - type: string; - - /** - * Resources newly created on the client _will always_ - * have an `lid` assigned immediately and available. - * @internal - */ - lid: string; -} - -export interface ResourceIdentifier { - lid: string; -} - -export type ResourceIdentifierObject = - | ResourceIdentifier - | ExistingResourceIdentifierObject - | NewResourceIdentifierObject; - -// TODO disallow NewResource, make narrowable -export interface SingleResourceRelationship { - data?: ExistingResourceIdentifierObject | NewResourceIdentifierObject | null; - meta?: Dict; - links?: Links; -} - -export interface CollectionResourceRelationship { - data?: Array; - meta?: Dict; - links?: PaginationLinks; -} - -/** - * Contains the data for an existing resource in JSON:API format - * @internal - */ -export interface ExistingResourceObject extends ExistingResourceIdentifierObject { - meta?: Dict; - attributes?: Dict; - relationships?: Dict; - links?: Links; -} - -interface Document { - meta?: Dict; - included?: ExistingResourceObject[]; - jsonapi?: Dict; - links?: Links | PaginationLinks; - errors?: JSONValue[]; -} - -export interface EmptyResourceDocument extends Document { - data: null; -} - -export interface SingleResourceDocument extends Document { - data: ExistingResourceObject; -} - -export interface CollectionResourceDocument extends Document { - data: ExistingResourceObject[]; -} - -export type JsonApiDocument = EmptyResourceDocument | SingleResourceDocument | CollectionResourceDocument; diff --git a/ember-data-types/q/fetch-manager.ts b/ember-data-types/q/fetch-manager.ts deleted file mode 100644 index baf1818ab63..00000000000 --- a/ember-data-types/q/fetch-manager.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Dict } from '@ember-data/types/q/utils'; - -import type { RecordIdentifier } from './identifier'; - -export interface Operation { - op: string; - options: Dict | undefined; - recordIdentifier: RecordIdentifier; -} - -export interface FindRecordQuery extends Operation { - op: 'findRecord'; - recordIdentifier: RecordIdentifier; - options: any; -} - -export interface SaveRecordMutation extends Operation { - op: 'saveRecord'; - recordIdentifier: RecordIdentifier; - options: any; -} - -export interface Request { - data: Operation[]; - options?: any; -} - -export type RequestStates = 'pending' | 'fulfilled' | 'rejected'; - -export interface RequestState { - state: RequestStates; - type: 'query' | 'mutation'; - request: Request; - response?: Response; -} - -export interface Response { - // rawData: unknown; - data: unknown; -} diff --git a/ember-data-types/q/identifier.ts b/ember-data-types/q/identifier.ts deleted file mode 100644 index 036ccfc4a59..00000000000 --- a/ember-data-types/q/identifier.ts +++ /dev/null @@ -1,279 +0,0 @@ -/** - @module @ember-data/store -*/ - -import { ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import { - DEBUG_CLIENT_ORIGINATED, - DEBUG_IDENTIFIER_BUCKET, -} from '@ember-data/store/-private/utils/identifier-debug-consts'; - -import type { ExistingResourceObject, ResourceIdentifierObject } from './ember-data-json-api'; - -export type ResourceData = ResourceIdentifierObject | ExistingResourceObject; -export type IdentifierBucket = 'record' | 'document'; - -export interface Identifier { - lid: string; - clientId?: string; -} - -export interface ExistingRecordIdentifier extends Identifier { - id: string; - type: string; -} - -export interface NewRecordIdentifier extends Identifier { - id: string | null; - type: string; -} - -/** - * An Identifier specific to a record which may or may not - * be present in the cache. - * - * The absence of an `id` DOES NOT indicate that this - * Identifier is for a new client-created record as it - * may also indicate that it was generated for a secondary - * index and the primary `id` index is not yet known. - * - * @internal - */ -export type RecordIdentifier = ExistingRecordIdentifier | NewRecordIdentifier; - -/** - * Used when an Identifier is known to be the stable version - * - * @internal - */ -export interface StableIdentifier extends Identifier { - [DEBUG_IDENTIFIER_BUCKET]?: string; -} - -/** - * Used when a StableRecordIdentifier was not created locally as part - * of a call to store.createRecord - * - * Distinguishing between this Identifier and one for a client created - * record that was created with an ID is generally speaking not possible - * at runtime, so anything with an ID typically narrows to this. - * - * @internal - */ -export interface StableExistingRecordIdentifier extends StableIdentifier { - id: string; - type: string; - [DEBUG_CLIENT_ORIGINATED]?: boolean; -} -/** - * Used when a StableRecordIdentifier was created locally - * (by a call to store.createRecord). - * - * It is possible in rare circumstances to have a StableRecordIdentifier - * that is not for a new record but does not have an ID. This would - * happen if a user intentionally created one for use with a secondary-index - * prior to the record having been fully loaded. - * - * @internal - */ -export interface StableNewRecordIdentifier extends StableIdentifier { - id: string | null; - type: string; - [DEBUG_CLIENT_ORIGINATED]?: boolean; -} - -/** - * A referentially stable object with a unique string (lid) that can be used - * as a reference to data in the cache. - * - * Every record instance has a unique identifier, and identifiers may refer - * to data that has never been loaded (for instance, in an async relationship). - * - * @class StableRecordIdentifier - * @public - */ - -/** - * A string representing a unique identity. - * - * @property {string} lid - * @public - */ -/** - * the primary resource `type` or `modelName` this identity belongs to. - * - * @property {string} type - * @public - */ -/** - * the primary id for the record this identity belongs to. `null` - * if not yet assigned an id. - * - * @property {string | null} id - * @public - */ -export type StableRecordIdentifier = StableExistingRecordIdentifier | StableNewRecordIdentifier; - -/** - Configures how unique identifier lid strings are generated by @ember-data/store. - - This configuration MUST occur prior to the store instance being created. - - Takes a method which can expect to receive various data as its first argument - and the name of a bucket as its second argument. - - Currently there are two buckets, 'record' and 'document'. - - ### Resource (`Record`) Identity - - If the bucket is `record` the method must return a unique (to at-least - the given bucket) string identifier for the given data as a string to be - used as the `lid` of an `Identifier` token. - - This method will only be called by either `getOrCreateRecordIdentifier` or - `createIdentifierForNewRecord` when an identifier for the supplied data - is not already known via `lid` or `type + id` combo and one needs to be - generated or retrieved from a proprietary cache. - - `data` will be the same data argument provided to `getOrCreateRecordIdentifier` - and in the `createIdentifierForNewRecord` case will be an object with - only `type` as a key. - - ```ts - import { setIdentifierGenerationMethod } from '@ember-data/store'; - - export function initialize(applicationInstance) { - // note how `count` here is now scoped to the application instance - // for our generation method by being inside the closure provided - // by the initialize function - let count = 0; - - setIdentifierGenerationMethod((resource, bucket) => { - return resource.lid || `my-key-${count++}`; - }); - } - - export default { - name: 'configure-ember-data-identifiers', - initialize - }; - ``` - - ### Document Identity - - If the bucket is `document` the method will receive the associated - immutable `request` passed to `store.request` as its first argument - and should return a unique string for the given request if the document - should be cached, and `null` if it should not be cached. - - Note, the request result will still be passed to the cache via `Cache.put`, - but caches should take this as a signal that the document should not itself - be cached, while its contents may still be used to update other cache state. - - The presence of `cacheOptions.key` on the request will take precedence - for the document cache key, and this method will not be called if it is - present. - - The default method implementation for this bucket is to return `null` - for all requests whose method is not `GET`, and to return the `url` for - those where it is. - - This means that queries via `POST` MUST provide `cacheOptions.key` or - implement this hook. - - ⚠️ Caution: Requests that do not have a `method` assigned are assumed to be `GET` - - @method setIdentifierGenerationMethod - @for @ember-data/store - @param method - @public - @static -*/ -export interface GenerationMethod { - (data: ImmutableRequestInfo, bucket: 'document'): string | null; - (data: ResourceData | { type: string }, bucket: 'record'): string; - (data: unknown, bucket: IdentifierBucket): string | null; -} - -/** - Configure a callback for when the identifier cache encounters new resource - data for an existing resource. - - This configuration MUST occur prior to the store instance being created. - - ```js - import { setIdentifierUpdateMethod } from '@ember-data/store'; - ``` - - Takes a method which can expect to receive an existing `Identifier` alongside - some new data to consider as a second argument. This is an opportunity - for secondary lookup tables and caches associated with the identifier - to be amended. - - This method is called everytime `updateRecordIdentifier` is called and - with the same arguments. It provides the opportunity to update secondary - lookup tables for existing identifiers. - - It will always be called after an identifier created with `createIdentifierForNewRecord` - has been committed, or after an update to the `record` a `RecordIdentifier` - is assigned to has been committed. Committed here meaning that the server - has acknowledged the update (for instance after a call to `.save()`) - - If `id` has not previously existed, it will be assigned to the `Identifier` - prior to this `UpdateMethod` being called; however, calls to the parent method - `updateRecordIdentifier` that attempt to change the `id` or calling update - without providing an `id` when one is missing will throw an error. - - @method setIdentifierUpdateMethod - @for @ember-data/store - @param method - @public - @static -*/ - -export type UpdateMethod = { - (identifier: StableRecordIdentifier, newData: ResourceData, bucket: 'record'): void; - (identifier: StableIdentifier, newData: unknown, bucket: never): void; -}; - -/** - Configure a callback for when the identifier cache is going to release an identifier. - - This configuration MUST occur prior to the store instance being created. - - ```js - import { setIdentifierForgetMethod } from '@ember-data/store'; - ``` - - Takes method which can expect to receive an existing `Identifier` that should be eliminated - from any secondary lookup tables or caches that the user has populated for it. - - @method setIdentifierForgetMethod - @for @ember-data/store - @param method - @public - @static -*/ -export type ForgetMethod = (identifier: StableIdentifier | StableRecordIdentifier, bucket: IdentifierBucket) => void; - -/** - Configure a callback for when the identifier cache is being torn down. - - This configuration MUST occur prior to the store instance being created. - - ```js - import { setIdentifierResetMethod } from '@ember-data/store'; - ``` - - Takes a method which can expect to be called when the parent application is destroyed. - - If you have properly used a WeakMap to encapsulate the state of your customization - to the application instance, you may not need to implement the `resetMethod`. - - @method setIdentifierResetMethod - @for @ember-data/store - @param method - @public - @static -*/ -export type ResetMethod = () => void; diff --git a/ember-data-types/q/record-data-json-api.ts b/ember-data-types/q/record-data-json-api.ts deleted file mode 100644 index ec61fafa333..00000000000 --- a/ember-data-types/q/record-data-json-api.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { - CollectionResourceRelationship, - Link, - Links, - Meta, - SingleResourceRelationship, -} from './ember-data-json-api'; -import type { Dict } from './utils'; - -/** - @module @ember-data/store -*/ - -export type AttributesHash = Dict; - -export interface JsonApiResource { - id?: string | null; - type?: string; - lid?: string; - attributes?: AttributesHash; - relationships?: Dict; - meta?: Meta; - links?: Links; -} - -export interface JsonApiError { - id?: string; - title?: string; - detail?: string; - links?: { - about?: Link; - type?: Link; - }; - status?: string; - code?: string; - source?: { - pointer: string; - parameter?: string; - header?: string; - }; - meta?: Meta; -} - -export type JsonApiRelationship = SingleResourceRelationship | CollectionResourceRelationship; diff --git a/ember-data-types/q/record-data-schemas.ts b/ember-data-types/q/record-data-schemas.ts deleted file mode 100644 index fcfe062acb2..00000000000 --- a/ember-data-types/q/record-data-schemas.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - @module @ember-data/store -*/ - -import type { Dict } from './utils'; - -export interface RelationshipSchema { - kind: 'belongsTo' | 'hasMany'; - type: string; // related type - /** - * @internal - * @deprecated - */ - key: string; // TODO @runspired remove our uses - // TODO @runspired should RFC be updated to make this optional? - // TODO @runspired sohuld RFC be update to enforce async and inverse are set? else internals need to know - // that meta came from @ember-data/model vs not from @ember-data/model as defaults should switch. - options: { - async?: boolean; // controls inverse unloading "client side delete semantics" so we should replace that with a real flag - polymorphic?: boolean; - inverse?: string | null; // property key on the related type (if any) - [key: string]: unknown; - }; - // inverse?: string | null; - // inverseIsAsync?: boolean; - name: string; // property key for this relationship -} - -export type RelationshipsSchema = Dict; - -export interface AttributeSchema { - /** - * @internal - */ - name: string; - - kind?: 'attribute'; - - // TODO @runspired update RFC to make options optional - options?: { - [key: string]: unknown; - }; - type?: string; // TODO @runspired update RFC to make type optional -} - -export type AttributesSchema = Dict; diff --git a/ember-data-types/q/record-instance.ts b/ember-data-types/q/record-instance.ts deleted file mode 100644 index 467f6cd2306..00000000000 --- a/ember-data-types/q/record-instance.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DSModel } from './ds-model'; -import type { Dict } from './utils'; -/** - @module @ember-data/store -*/ - -/* - A `Record` is the result of the store instantiating a class to present data for a resource to the UI. - - Historically in `ember-data` this meant that it was the result of calling `ModelFactory.create()` to - gain instance to a class built upon `@ember-data/model`. However, as we go forward into a future in which - model instances (aka `Records`) are completely user supplied and opaque to the internals, we need a type - through which to communicate what is valid. - - The type belows allows for anything extending object. -*/ - -export type RecordInstance = DSModel | Dict; diff --git a/ember-data-types/q/schema-service.ts b/ember-data-types/q/schema-service.ts deleted file mode 100644 index 6ad7b8de503..00000000000 --- a/ember-data-types/q/schema-service.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - @module @ember-data/store -*/ - -import type { RecordIdentifier } from './identifier'; -import type { AttributesSchema, RelationshipsSchema } from './record-data-schemas'; - -/** - * A SchemaDefinitionService implementation provides the ability - * to query for various information about a resource in an abstract manner. - * - * How an implementation determines this information is left up to the implementation, - * this means that schema information could be lazily populated, derived-on-demand, - * or progressively enhanced during the course of an application's runtime. - * - * The implementation provided to work with `@ember-data/model` makes use of the - * static schema properties on those classes to respond to these queries; however, - * that is not a necessary approach. For instance, Schema information could be sideloaded - * or pre-flighted for API calls, resulting in no need to bundle and ship potentially - * large and expensive JSON or JS schemas to pull information from. - * - * To register a custom schema implementation, extend the store service or - * lookup and register the schema service first thing on app-boot. Example below - * shows extending the service. - * - * ```ts - * import Store from '@ember-data/store'; - * import CustomSchemas from './custom-schemas'; - * - * export default class extends Store { - * constructor(...args) { - * super(...args); - * this.registerSchemaDefinitionService(new CustomSchemas()); - * } - * } - * ``` - * - * At runtime, both the `Store` and the `StoreWrapper` provide - * access to this service via the `getSchemaDefinitionService()` method. - * - * ```ts - * export default class extends Component { - * @service store; - * - * get attributes() { - * return this.store - * .getSchemaDefinitionService() - * .attributesDefinitionFor(this.args.dataType); - * } - * } - * ``` - * - * This is not a class and cannot be instantiated. - * - * @class SchemaService - * @public - */ -export interface SchemaService { - /** - * Queries whether the schema-definition-service recognizes `type` as a resource type - * - * @method doesTypeExist - * @public - * @param {string} type - * @returns {boolean} - */ - doesTypeExist(type: string): boolean; - - /** - * Returns definitions for all properties of the specified resource - * that are considered "attributes". Generally these are properties - * that are not related to book-keeping state on the client and do - * not represent a linkage to another resource. - * - * The return value should be a dictionary of key:value pairs - * where the `key` is the attribute or property's name and `value` - * is an object with at least the property `name` which should also - * match `key`. - * - * Optionally, this object may also specify `type`, which should - * be a string reference to a `transform`, and `options` which - * should be dictionary in which any key:value pairs are permissable. - * - * For instance, when using `@ember-data/model`, the following attribute - * definition: - * - * ```ts - * class extends Model { - * @attr('string', { defaultValue: 'hello' }) greeting; - * @attr('date') birthday; - * @attr firstName; - * } - * ``` - * - * Would be returned as: - * - * ```js - * { - * greeting: { name: 'greeting', type: 'string', options: { defaultValue: 'hello' } }, - * birthday: { name: 'birthday', type: 'date' }, - * firstName: { name: 'firstName' } - * } - * ``` - * - * @method attributesDefinitionFor - * @public - * @param {RecordIdentifier|{ type: string }} identifier - * @returns {AttributesSchema} - */ - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema; - - /** - * Returns definitions for all properties of the specified resource - * that are considered "relationships". Generally these are properties - * that represent a linkage to another resource. - * - * The return value should be a dictionary of key:value pairs - * where the `key` is the relationship or property's name and `value` - * is an object with at least the following properties: - * - * - `name` which should also match the `key` used in the dictionary. - * - `kind` which should be either `belongsTo` or `hasMany` - * - `type` which should be the related resource's string "type" - * - `options` which should be a dictionary allowing any key but with - * at least the below keys present. - * - * - `options.async` a boolean representing whether data for this relationship is - * typically loaded on-demand. - * - `options.inverse` a string or null representing the field name / key of the - * corresponding relationship on the inverse resource. - * - * Additionally the following options properties are optional. See [Polymorphic Relationships](https://rfcs.emberjs.com/id/0793-polymporphic-relations-without-inheritance) - * - * - `options.polymorphic` a boolean representing whether multiple resource types - * can be used to satisfy this relationship. - * - `options.as` a string representing the abstract type that the concrete side of - * a relationship must specify when fulfilling a polymorphic inverse. - * - * For example, the following Model using @ember-data/model would generate this relationships - * definition by default: - * - * ```js - * class User extends Model { - * @belongsTo('user', { async: false, inverse: null }) bestFriend; - * @hasMany('user', { async: true, inverse: 'friends' }) friends; - * @hasMany('pet', { async: false, polymorphic: true, inverse: 'owner' }) pets; - * } - * ``` - * - * Which would be returned as - * - * ```js - * { - * bestFriend: { - * name: 'bestFriend', - * kind: 'belongsTo', - * type: 'user', - * options: { - * async: false, - * inverse: null - * } - * }, - * friends: { - * name: 'friends', - * kind: 'hasMany', - * type: 'user', - * options: { - * async: true, - * inverse: 'friends' - * } - * }, - * pets: { - * name: 'pets', - * kind: 'hasMany', - * type: 'pet', - * options: { - * async: false, - * polymorphic: true, - * inverse: 'owner' - * } - * }, - * } - * ``` - * - * @method relationshipsDefinitionFor - * @public - * @param {RecordIdentifier|{ type: string }} identifier - * @returns {RelationshipsSchema} - */ - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema; -} diff --git a/ember-data-types/q/store.ts b/ember-data-types/q/store.ts deleted file mode 100644 index a2c01ab4b29..00000000000 --- a/ember-data-types/q/store.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Dict } from './utils'; - -export interface FindOptions { - reload?: boolean; - backgroundReload?: boolean; - include?: string; - adapterOptions?: Dict; - preload?: Dict; -} diff --git a/ember-data-types/q/utils.ts b/ember-data-types/q/utils.ts deleted file mode 100644 index 43d92465412..00000000000 --- a/ember-data-types/q/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - @module @ember-data/store -*/ - -export type ConfidentDict = { [key: string]: V }; -export type Dict = { [key: string]: V | undefined }; diff --git a/package.json b/package.json index c26df05903d..e438083270f 100644 --- a/package.json +++ b/package.json @@ -2,117 +2,83 @@ "name": "root", "version": "4.12.8", "private": true, + "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com:emberjs/data.git" }, "scripts": { + "takeoff": "FORCE_COLOR=2 pnpm install --prefer-offline --reporter=append-only", + "prepare": "turbo run build:infra; pnpm --filter './packages/*' run --parallel --if-present sync-hardlinks; turbo run build:pkg; pnpm run prepare:types; pnpm run _task:sync-hardlinks;", + "prepare:types": "tsc --build --force; turbo run build:glint;", + "release": "./release/index.ts", + "build": "turbo _build --log-order=stream --filter=./packages/* --concurrency=10;", + "_task:sync-hardlinks": "pnpm run -r --parallel --if-present sync-hardlinks;", "build:docs": "mkdir -p packages/-ember-data/dist && cd ./docs-generator && node ./compile-docs.js", - "lint:js": "eslint --quiet --cache --cache-strategy=content --ext=js,ts .", + "lint:tests": "turbo --log-order=stream lint --filter=./tests/* --continue --concurrency=10", + "lint:pkg": "turbo --log-order=stream lint --filter=./packages/* --continue --concurrency=10", + "lint": "pnpm run _task:sync-hardlinks; turbo --log-order=stream lint --continue --concurrency=10", + "lint:fix": "pnpm run _task:sync-hardlinks; turbo --log-order=stream lint --continue --concurrency=10 -- --fix", + "lint:prettier": "prettier --check --cache --cache-location=.prettier-cache --log-level=warn .", + "lint:prettier:fix": "prettier --write --cache --cache-location=.prettier-cache --log-level=warn .", "preinstall": "npx only-allow pnpm", - "problems": "tsc -p tsconfig.json --noEmit --pretty false", - "test": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app --filter request-test-app run test", - "test:production": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app run test -e production", + "check:test-types": "turbo --log-order=stream check:types --filter=./{tests,config}/* --continue --concurrency=10", + "check:types": "pnpm run _task:sync-hardlinks; bun run check:test-types", + "test": "pnpm run _task:sync-hardlinks; pnpm turbo test --concurrency=1", + "test:production": "pnpm run _task:sync-hardlinks; pnpm turbo test:production --concurrency=1", "test:try-one": "pnpm --filter main-test-app run test:try-one", - "test:docs": "pnpm build:docs && pnpm --filter docs-tests test", - "test:encapsulation": "pnpm --filter '*-encapsulation-test-app' run test", - "test:fastboot": "pnpm --filter fastboot-test-app test", - "test:embroider": "pnpm --filter embroider-basic-compat test", - "test:infra": "pnpm --filter @ember-data/unpublished-test-infra test", - "test-external:ember-m3": "node ./scripts/test-external-partner-project.js ember-m3 https://github.com/hjdivad/ember-m3.git", - "test-external:ember-data-change-tracker": "node ./scripts/test-external-partner-project.js ember-data-change-tracker https://github.com/danielspaniel/ember-data-change-tracker.git", - "test-external:model-fragments": "node ./scripts/test-external-partner-project.js model-fragments https://github.com/lytics/ember-data-model-fragments.git", - "test-external:ember-observer": "node ./scripts/test-external-partner-project.js ember-observer https://github.com/emberobserver/client.git --noLockFile", - "test-external:travis-web": "node ./scripts/test-external-partner-project.js travis-web https://github.com/travis-ci/travis-web.git", - "test-external:storefront": "node ./scripts/test-external-partner-project.js storefront https://github.com/embermap/ember-data-storefront.git", - "test-external:factory-guy": "node ./scripts/test-external-partner-project.js factory-guy https://github.com/adopted-ember-addons/ember-data-factory-guy.git", - "test-external:ilios-frontend": "node ./scripts/test-external-partner-project.js ilios-frontend https://github.com/ilios/frontend.git --skipSmokeTest", - "test-external:ember-resource-metadata": "node ./scripts/test-external-partner-project.js ember-resource-metadata https://github.com/ef4/ember-resource-metadata.git --noLockFile", - "test-external:ember-data-relationship-tracker": "node ./scripts/test-external-partner-project.js ember-data-relationship-tracker https://github.com/ef4/ember-data-relationship-tracker.git" + "test:docs": "FORCE_COLOR=2 pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present --reporter=append-only --reporter-hide-prefix test:docs", + "test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints", + "test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot", + "test:embroider": "pnpm run -r ---workspace-concurrency=-1 --if-present test:embroider", + "test:vite": "pnpm run -r ---workspace-concurrency=-1 --if-present test:vite" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/eslint-parser": "^7.21.3", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/runtime": "^7.21.0", - "@ember/edition-utils": "^1.2.0", - "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.9.3", + "@babel/core": "^7.24.5", + "@ember/test-helpers": "3.3.0", "@glimmer/component": "^1.1.2", - "@glimmer/env": "^0.1.7", - "@types/ember": "^4.0.3", - "@types/ember-qunit": "^5.0.2", - "@types/ember-resolver": "^5.0.13", - "@types/ember-testing-helpers": "^0.0.4", - "@types/ember__application": "^4.0.5", - "@types/ember__array": "^4.0.3", - "@types/ember__component": "^4.0.12", - "@types/ember__controller": "^4.0.4", - "@types/ember__debug": "^4.0.3", - "@types/ember__engine": "^4.0.4", - "@types/ember__error": "^4.0.2", - "@types/ember__object": "^4.0.5", - "@types/ember__polyfills": "^4.0.1", - "@types/ember__routing": "^4.0.12", - "@types/ember__runloop": "^4.0.2", - "@types/ember__service": "^4.0.2", - "@types/ember__string": "^3.0.10", - "@types/ember__template": "^4.0.1", - "@types/ember__test": "^4.0.1", - "@types/ember__utils": "^4.0.2", - "@types/htmlbars-inline-precompile": "^3.0.0", - "@types/qunit": "^2.19.4", - "@types/rsvp": "^4.0.4", - "@typescript-eslint/eslint-plugin": "^5.57.1", - "@typescript-eslint/parser": "^5.57.1", + "@glint/core": "1.5.0", + "@glint/environment-ember-loose": "1.5.0", + "@glint/environment-ember-template-imports": "1.5.0", + "@glint/template": "1.5.0", + "@types/semver": "^7.5.8", + "badge-maker": "4.1.0", + "bun-types": "^1.1.30", "chalk": "^4.1.2", "co": "^4.6.0", "command-line-args": "^5.2.1", + "comment-json": "^4.2.5", "common-tags": "^1.8.2", - "ember-cli": "~4.11.0", - "ember-source": "~4.12.0", - "debug": "^4.3.4", - "eslint": "^8.37.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-ember": "^11.4.9", - "eslint-plugin-ember-data": "link:./packages/unpublished-eslint-rules", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-mocha": "^10.1.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-qunit": "^7.3.4", - "eslint-plugin-simple-import-sort": "^10.0.0", - "execa": "^5.1.1", - "fromentries": "^1.3.2", - "git-repo-info": "^2.1.1", + "debug": "^4.3.7", + "ember-source": "~5.12.0", + "execa": "^9.4.1", "git-repo-version": "^1.0.2", - "glob": "^9.3.4", - "json-typescript": "^1.1.2", + "globby": "^14.0.2", "lerna-changelog": "^2.2.0", - "loader.js": "^4.7.0", - "prettier": "^2.8.7", - "rimraf": "^4.4.1", - "semver": "^7.3.8", + "prettier": "^3.3.2", + "prettier-plugin-ember-template-tag": "^2.0.2", + "rimraf": "^5.0.6", + "semver": "^7.6.3", "silent-error": "^1.1.1", - "testem": "^3.10.1", - "typescript": "~5.0.3", - "url": "^0.11.0", - "webpack": "^5.77.0", + "typescript": "^5.4.5", + "url": "^0.11.4", "yuidocjs": "^0.10.2", "zlib": "1.0.5" }, + "dependencies": { + "turbo": "^1.13.4" + }, "engines": { - "node": "16.* || >= 18.*", + "node": ">= 18.20.4", "yarn": "use pnpm", "npm": "use pnpm", - "pnpm": "9.7.1" + "pnpm": "8.15.9" }, "volta": { - "node": "18.15.0", - "pnpm": "9.7.1" + "node": "22.3.0", + "pnpm": "8.15.9" }, - "packageManager": "pnpm@9.7.1", + "packageManager": "pnpm@8.15.9", "changelog": { "labels": { ":label: breaking": ":boom: Breaking Change", @@ -127,13 +93,87 @@ } }, "pnpm": { + "packageExtensions": { + "@glimmer/syntax": { + "dependencies": { + "@glimmer/env": "^0.1.7" + } + }, + "ember-cli-blueprint-test-helpers": { + "peerDependencies": { + "ember-cli": "*" + } + }, + "ember-page-title": { + "peerDependencies": { + "@glimmer/component": "*", + "ember-source": "*" + } + }, + "ember-cli-fastboot": { + "peerDependencies": { + "ember-cli": "*", + "ember-source": "*" + } + }, + "ember-cli-fastboot-testing": { + "peerDependencies": { + "ember-cli": "*", + "ember-cli-fastboot": "*", + "ember-source": "*" + } + }, + "ember-auto-import": { + "dependencies": { + "webpack": "*" + } + }, + "ember-source": { + "dependencies": { + "webpack": "*" + }, + "peerDependencies": { + "@glimmer/component": "*" + } + }, + "@ember/test-helpers": { + "dependencies": { + "webpack": "*" + } + } + }, "overrides": { - "ember-auto-import": "^2.6.1", - "@embroider/macros": "1.10.0", + "ember-auto-import": "^2.10.0", "broccoli-funnel": "^3.0.8", "broccoli-merge-trees": "^4.2.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-htmlbars": "^6.2.0" + "@glimmer/validator": "^0.92.3", + "@glint/core": "1.5.0", + "@glint/environment-ember-loose": "1.5.0", + "@glint/environment-ember-template-imports": "1.5.0", + "@glint/template": "1.5.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-typescript": "^5.3.0", + "webpack": "5.94.0", + "qunit": "2.19.4", + "ember-compatibility-helpers": "^1.2.7", + "testem": "~3.11.0" + }, + "peerDependencyRules": { + "ignoreMissing": [ + "rsvp" + ], + "allowAny": [ + "ember-source", + "@glimmer/component", + "typescript" + ] + }, + "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" } } } diff --git a/packages/-ember-data/.npmignore b/packages/-ember-data/.npmignore deleted file mode 100644 index d4910ea2c6f..00000000000 --- a/packages/-ember-data/.npmignore +++ /dev/null @@ -1,45 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts -yarn-error.log - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/bin -/docs -/node-tests -.appveyor.yml -lib/scripts - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json diff --git a/packages/-ember-data/CHANGELOG.md b/packages/-ember-data/CHANGELOG.md new file mode 100644 index 00000000000..6ca2b57689a --- /dev/null +++ b/packages/-ember-data/CHANGELOG.md @@ -0,0 +1,75 @@ +# ember-data 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)) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@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)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) +* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9469](https://github.com/emberjs/data/pull/9469) Fix exports for 'ember-data' ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) +* [#9318](https://github.com/emberjs/data/pull/9318) fix: be more specific in files in case .npmignore is ignored ([@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)) +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +[@NullVoxPopuli](https://github.com/NullVoxPopuli) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) +* [#9069](https://github.com/emberjs/data/pull/9069) feat: Improve extensibility ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#8892](https://github.com/emberjs/data/pull/8892) doc: Fix paths in transform deprecations ([@HeroicEric](https://github.com/HeroicEric)) + +#### :house: Internal + +* [#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)) +* [#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)) +* [#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)) +* [#9021](https://github.com/emberjs/data/pull/9021) chore: cleanup ember-data/-private types ([@runspired](https://github.com/runspired)) +* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (3) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/-ember-data/README.md b/packages/-ember-data/README.md index b54882ef52b..6a13df8cfbf 100644 --- a/packages/-ember-data/README.md +++ b/packages/-ember-data/README.md @@ -31,6 +31,14 @@ Wrangle your application's data management with scalable patterns for developer - 🐹 Built with ♥️ by [Ember](https://emberjs.com) - ⚛️ Supports any API: `GraphQL` `JSON:API` `REST` `tRPC` ...bespoke or a mix +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/ember-data/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/ember-data/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/ember-data/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/ember-data/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/ember-data/lts-4-12?label=%40lts-4-12&color=bbbbbb) + ### 📖 On This Page - [Overview](#overview) @@ -134,12 +142,8 @@ activate this polyfill: ```ts let app = new EmberApp(defaults, { - '@embroider/macros': { - setConfig: { - '@ember-data/store': { - polyfillUUID: true, - }, - }, + emberData: { + polyfillUUID: true, }, }); ``` diff --git a/packages/-ember-data/addon-main.cjs b/packages/-ember-data/addon-main.cjs new file mode 100644 index 00000000000..b147fbad7b2 --- /dev/null +++ b/packages/-ember-data/addon-main.cjs @@ -0,0 +1,39 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +const addon = addonShim(__dirname); +const pkg = require('./package.json'); +if (pkg['ember-addon'].version === 1) { + delete addon.treeForApp; +} + +function findApp(addon) { + let current = addon; + let app; + + // Keep iterating upward until we don't have a grandparent. + // Has to do this grandparent check because at some point we hit the project. + do { + app = current.app || app; + } while (current.parent.parent && (current = current.parent)); + + return app; +} + +const included = addon.included; +addon.included = function includedIntercept() { + // we access this as a side-effect to ember-cli will give us a super call + const sup = this._super.included; + if (this.hasBeenCalled) { + return included?.apply(this, arguments); + } + this.hasBeenCalled = true; + const app = findApp(this); + const dirname = app.project.root; + const { setConfig } = require('@warp-drive/build-config/cjs-set-config.cjs'); + setConfig(app, dirname, Object.assign({}, app.options?.emberData, { ___legacy_support: true })); + return included?.apply(this, arguments); +}; + +module.exports = addon; diff --git a/packages/-ember-data/addon-test-support/index.js b/packages/-ember-data/addon-test-support/index.js deleted file mode 100644 index 96baceef96b..00000000000 --- a/packages/-ember-data/addon-test-support/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import { assert } from '@ember/debug'; -import { render as renderTemplate, settled } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; - -import { PRODUCTION } from '@ember-data/env'; - -/* - Temporary replacement for the render test helper - which we will deprecate in EmberData 5.0, this allows - an app to incrementally migrate to tests that render async - relationships in stages with potential for tests in between. -*/ -export async function render(template) { - await renderTemplate(template); - const owner = QUnit.config.current.testEnvironment.owner; - const pending = owner.lookup('service:store')._getAllPending(); - - // this should only be necessary in production tests - // where @ember/test-waiters is deactivated :() - if (PRODUCTION) { - assert( - `No pending requests exist in this test, use \`import { render } from '@ember/test-helpers';\``, - pending.length - ); - - await pending; - await settled(); - } -} diff --git a/packages/-ember-data/addon/-private/core.js b/packages/-ember-data/addon/-private/core.js deleted file mode 100644 index 185417e53a5..00000000000 --- a/packages/-ember-data/addon/-private/core.js +++ /dev/null @@ -1,15 +0,0 @@ -import Namespace from '@ember/application/namespace'; -import Ember from 'ember'; - -import VERSION from 'ember-data/version'; - -const DS = Namespace.create({ - VERSION: VERSION, - name: 'DS', -}); - -if (Ember.libraries) { - Ember.libraries.registerCoreLibrary('Ember Data', VERSION); -} - -export default DS; diff --git a/packages/-ember-data/addon/-private/index.ts b/packages/-ember-data/addon/-private/index.ts deleted file mode 100644 index 6471ba8734e..00000000000 --- a/packages/-ember-data/addon/-private/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// public -import ArrayProxy from '@ember/array/proxy'; -import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -import ObjectProxy from '@ember/object/proxy'; - -import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; -import RequestManager from '@ember-data/request'; -import Fetch from '@ember-data/request/fetch'; -import BaseStore, { CacheHandler } from '@ember-data/store'; - -export class Store extends BaseStore { - constructor(args: Record) { - super(args); - this.requestManager = new RequestManager(); - this.requestManager.use([LegacyNetworkHandler, Fetch]); - this.requestManager.useCache(CacheHandler); - } -} - -export { default as DS } from './core'; -export { Errors } from '@ember-data/model/-private'; -export { Snapshot } from '@ember-data/legacy-compat/-private'; - -// `ember-data-model-fragments' and `ember-data-change-tracker` rely on `normalizeModelName` -export { RecordArrayManager, normalizeModelName, coerceId } from '@ember-data/store/-private'; -export { ManyArray, PromiseManyArray } from '@ember-data/model/-private'; -export { SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; - -export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin); -export const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/-ember-data/addon/index.js b/packages/-ember-data/addon/index.js deleted file mode 100644 index 2cdbaf3ed22..00000000000 --- a/packages/-ember-data/addon/index.js +++ /dev/null @@ -1,272 +0,0 @@ -/** -

- -

- -

The lightweight reactive data library for JavaScript applications

- ---- - -Wrangle your application's data management with scalable patterns for developer productivity. - -- ⚡️ Committed to Best-In-Class Performance -- 🌲 Focused on being as svelte as possible -- 🚀 SSR Ready -- 🔜 Typescript Support -- 🐹 Built with ♥️ by [Ember](https://emberjs.com) -- ⚛️ Supports any API: `GraphQL` `JSON:API` `REST` `tRPC` ...bespoke or a mix - -### 📖 On This Page - -- [Overview](./#overview) - - [Architecture](#🪜-architecture) - - [Basic Installation](#basic-installation) - - [Advanced Installation](#advanced-installation) -- [Configuration](#configuration) - - [Deprecation Stripping](#deprecation-stripping) - - [randomUUID polyfill](#randomuuid-polyfill) - - [Removing inspector support in production](#removing-inspector-support-in-production) - - [Debugging](#debugging) - - -# Overview - -*Ember*‍**Data** is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. - -## 🪜 Architecture - -The core of *Ember*‍**Data** is the `Store`, which coordinates interaction between your application, the `Cache`, and sources of data (such as your `API` or a local persistence layer). -Optionally, the Store can be configured to hydrate the response data into rich presentation classes. - -*Ember*‍**Data** is both resource centric and document centric in it's approach to caching, requesting and presenting data. Your application's configuration and usage drives which is important and when. - -The `Store` is a **coordinator**. When using a `Store` you configure what cache to use, how cache data should be presented to the UI, and where it should look for requested data when it is not available in the cache. - -This coordination is handled opaquely to the nature of the requests issued and the format of the data being handled. This approach gives applications broad flexibility to configure *Ember*‍**Data** to best suite their needs. This makes *Ember*‍**Data** a powerful solution for applications regardless of their size and complexity. - -*Ember*‍**Data** is designed to scale, with a religious focus on performance and asset-size to keep its footprint small but speedy while still being able to handle large complex APIs in huge data-driven applications with no additional code and no added application complexity. It's goal is to prevent applications from writing code to manage data that is difficult to maintain or reason about. - -*Ember*‍**Data**'s power comes not from specific features, data formats, or adherence to specific API specs such as `JSON:API` `trpc` or `GraphQL`, but from solid conventions around requesting and mutating data developed over decades of experience scaling developer productivity. - -## Basic Installation - -Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) - -```no-highlight -pnpm add ember-data -``` - -`ember-data` is installed by default for new applications generated with `ember-cli`. You can check what version is installed by looking in the `devDependencies` hash of your project's [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) file. - -If you have generated a new `Ember` application using `ember-cli` but do -not wish to use `ember-data`, remove `ember-data` from your project's `package.json` file and run your package manager's install command to update your lockfile. - -## Advanced Installation - -*Ember*‍**Data** is organized into primitives that compose together via public APIs. - -- [@ember-data/store](/ember-data/release/modules/@ember-data%2Fstore) is the core and handles coordination -- [@ember-data/json-api](/ember-data/release/modules/@ember-data%2Fjson-api) provides a resource cache for JSON:API structured data. It integrates with the store via the hook `createCache` -- [@ember-data/model](/ember-data/release/modules/@ember-data%2Fmodel) is a presentation layer, it integrates with the store via the hooks `instantiateRecord` and `teardownRecord`. -- [@ember-data/adapter](/ember-data/release/modules/@ember-data%2Fadapter) provides various network API integrations for APIS built over specific REST or JSON:API conventions. -- [@ember-data/serializer](/ember-data/release/modules/@ember-data%2Fserializer) pairs with `@ember-data/adapter` to normalize and serialize data to and from an API format into the `JSON:API` format understood by `@ember-data/json-api`. -- [@ember-data/debug](/ember-data/release/modules/@ember-data%2Fdebug) provides debugging support for the `ember-inspector`. -- **ember-data** is a "meta" package which bundles all of these together for convenience - -The packages interop with each other through well defined public API boundaries. The core -of the library is the store provided by `@ember-data/store`, while each of the other libraries plugs into the store when installed. Because these packages interop via fully -public APIs, other libraries or applications may provide their own implementations. For instance, [ember-m3](https://github.com/hjdivad/ember-m3) is a commonly used presentation and cache implementation suitable for complex resource objects and graphs. - -## Configuration - -### Deprecation Stripping - -*Ember*‍**Data** allows users to opt-in and remove code that exists to support deprecated behaviors. - -If your app has resolved all deprecations present in a given version, you may specify that version as your "compatibility" version to remove the code that supported the deprecated behavior from your app. - -```ts -let app = new EmberApp(defaults, { - emberData: { - compatWith: '4.8', - }, -}); -``` - -- [Full Documentation](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fdeprecations) - -### randomUUID polyfill - -*Ember*‍**Data** uses `UUID V4` by default to generate identifiers for new data created on the client. Identifier generation is configurable, but we also for convenience will polyfill -the necessary feature if your browser support or deployment environment demands it. To -activate this polyfill: - -```ts -let app = new EmberApp(defaults, { - '@embroider/macros': { - setConfig: { - '@ember-data/store': { - polyfillUUID: true - }, - }, - }, -}); -``` - -### removing inspector support in production - -If you do not with to ship inspector support in your production application, you can specify -that all support for it should be stripped from the build. - -```ts -let app = new EmberApp(defaults, { - emberData: { - includeDataAdapterInProduction: false - } -}); -``` - -- [Full Documentation](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fdebug) - -### Debugging - -Many portions of the internals are helpfully instrumented with logging that can be activated -at build time. This instrumentation is always removed from production builds or any builds -that has not explicitly activated it. To activate it set the appropriate flag to `true`. - -```ts - let app = new EmberApp(defaults, { - emberData: { - debug: { - LOG_PAYLOADS: false, // data store received to update cache with - LOG_OPERATIONS: false, // updates to cache remote state - LOG_MUTATIONS: false, // updates to cache local state - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, // log Requests issued via the request manager - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - } - } - }); - ``` - - @module ember-data-overview - @main ember-data-overview -*/ -import 'ember-inflector'; - -import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; - -import Adapter, { BuildURLMixin } from '@ember-data/adapter'; -import AdapterError, { - AbortError, - ConflictError, - errorsArrayToHash, - errorsHashToArray, - ForbiddenError, - InvalidError, - NotFoundError, - ServerError, - TimeoutError, - UnauthorizedError, -} from '@ember-data/adapter/error'; -import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import RESTAdapter from '@ember-data/adapter/rest'; -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import Serializer from '@ember-data/serializer'; -import { BooleanTransform, DateTransform, NumberTransform, StringTransform } from '@ember-data/serializer/-private'; -import JSONSerializer from '@ember-data/serializer/json'; -import JSONAPISerializer from '@ember-data/serializer/json-api'; -import RESTSerializer, { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; -import Transform from '@ember-data/serializer/transform'; -import { normalizeModelName } from '@ember-data/store'; - -import { - DS, - Errors, - ManyArray, - PromiseArray, - PromiseManyArray, - PromiseObject, - RecordArrayManager, - Snapshot, - Store, -} from './-private'; -import setupContainer from './setup-container'; - -DS.Store = Store; -DS.PromiseArray = PromiseArray; -DS.PromiseObject = PromiseObject; - -DS.PromiseManyArray = PromiseManyArray; - -DS.Model = Model; -DS.attr = attr; -DS.Errors = Errors; - -DS.Snapshot = Snapshot; - -DS.Adapter = Adapter; - -DS.AdapterError = AdapterError; -DS.InvalidError = InvalidError; -DS.TimeoutError = TimeoutError; -DS.AbortError = AbortError; - -DS.UnauthorizedError = UnauthorizedError; -DS.ForbiddenError = ForbiddenError; -DS.NotFoundError = NotFoundError; -DS.ConflictError = ConflictError; -DS.ServerError = ServerError; - -DS.errorsHashToArray = errorsHashToArray; -DS.errorsArrayToHash = errorsArrayToHash; - -DS.Serializer = Serializer; - -if (macroCondition(dependencySatisfies('@ember-data/debug', '*'))) { - DS.DebugAdapter = importSync('@ember-data/debug').default; -} - -DS.ManyArray = ManyArray; - -DS.RecordArrayManager = RecordArrayManager; - -DS.RESTAdapter = RESTAdapter; -DS.BuildURLMixin = BuildURLMixin; - -DS.RESTSerializer = RESTSerializer; -DS.JSONSerializer = JSONSerializer; - -DS.JSONAPIAdapter = JSONAPIAdapter; -DS.JSONAPISerializer = JSONAPISerializer; - -DS.Transform = Transform; -DS.DateTransform = DateTransform; -DS.StringTransform = StringTransform; -DS.NumberTransform = NumberTransform; -DS.BooleanTransform = BooleanTransform; - -DS.EmbeddedRecordsMixin = EmbeddedRecordsMixin; - -DS.belongsTo = belongsTo; -DS.hasMany = hasMany; - -DS._setupContainer = setupContainer; - -Object.defineProperty(DS, 'normalizeModelName', { - enumerable: true, - writable: false, - configurable: false, - value: normalizeModelName, -}); - -export default DS; diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts deleted file mode 100644 index ae380a10821..00000000000 --- a/packages/-ember-data/addon/store.ts +++ /dev/null @@ -1 +0,0 @@ -export { Store as default } from './-private'; diff --git a/packages/-ember-data/app/transforms/boolean.js b/packages/-ember-data/app/transforms/boolean.js index f32800a7157..b4d4471fa7f 100644 --- a/packages/-ember-data/app/transforms/boolean.js +++ b/packages/-ember-data/app/transforms/boolean.js @@ -1 +1 @@ -export { BooleanTransform as default } from '@ember-data/serializer/-private'; +export { BooleanTransform as default } from '@ember-data/serializer/transform'; diff --git a/packages/-ember-data/app/transforms/date.js b/packages/-ember-data/app/transforms/date.js index d5a3eff02da..4aa235dc618 100644 --- a/packages/-ember-data/app/transforms/date.js +++ b/packages/-ember-data/app/transforms/date.js @@ -1 +1 @@ -export { DateTransform as default } from '@ember-data/serializer/-private'; +export { DateTransform as default } from '@ember-data/serializer/transform'; diff --git a/packages/-ember-data/app/transforms/number.js b/packages/-ember-data/app/transforms/number.js index dedc67a0ed6..47e4c0731a7 100644 --- a/packages/-ember-data/app/transforms/number.js +++ b/packages/-ember-data/app/transforms/number.js @@ -1 +1 @@ -export { NumberTransform as default } from '@ember-data/serializer/-private'; +export { NumberTransform as default } from '@ember-data/serializer/transform'; diff --git a/packages/-ember-data/app/transforms/string.js b/packages/-ember-data/app/transforms/string.js index 974d19693c9..ba881680d44 100644 --- a/packages/-ember-data/app/transforms/string.js +++ b/packages/-ember-data/app/transforms/string.js @@ -1 +1 @@ -export { StringTransform as default } from '@ember-data/serializer/-private'; +export { StringTransform as default } from '@ember-data/serializer/transform'; diff --git a/packages/-ember-data/babel.config.mjs b/packages/-ember-data/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/-ember-data/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/-ember-data/blueprints/adapter-test/index.js b/packages/-ember-data/blueprints/adapter-test/index.js index 0285c42143b..e774f4039d8 100644 --- a/packages/-ember-data/blueprints/adapter-test/index.js +++ b/packages/-ember-data/blueprints/adapter-test/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/adapter/blueprints/adapter-test'); +module.exports = require('@ember-data/adapter/blueprints/adapter-test/index'); diff --git a/packages/-ember-data/blueprints/adapter/index.js b/packages/-ember-data/blueprints/adapter/index.js index da0a6c102c1..10cbcd08363 100644 --- a/packages/-ember-data/blueprints/adapter/index.js +++ b/packages/-ember-data/blueprints/adapter/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/adapter/blueprints/adapter'); +module.exports = require('@ember-data/adapter/blueprints/adapter/index'); diff --git a/packages/-ember-data/blueprints/model-test/index.js b/packages/-ember-data/blueprints/model-test/index.js index 3b54452d580..508769620a9 100644 --- a/packages/-ember-data/blueprints/model-test/index.js +++ b/packages/-ember-data/blueprints/model-test/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/model/blueprints/model-test'); +module.exports = require('@ember-data/model/blueprints/model-test/index'); diff --git a/packages/-ember-data/blueprints/model/index.js b/packages/-ember-data/blueprints/model/index.js index 02f9819b4f7..57a8ec2ac8d 100644 --- a/packages/-ember-data/blueprints/model/index.js +++ b/packages/-ember-data/blueprints/model/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/model/blueprints/model'); +module.exports = require('@ember-data/model/blueprints/model/index'); diff --git a/packages/-ember-data/blueprints/serializer-test/index.js b/packages/-ember-data/blueprints/serializer-test/index.js index 928704fce18..bb00c002fd3 100644 --- a/packages/-ember-data/blueprints/serializer-test/index.js +++ b/packages/-ember-data/blueprints/serializer-test/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/serializer/blueprints/serializer-test'); +module.exports = require('@ember-data/serializer/blueprints/serializer-test/index'); diff --git a/packages/-ember-data/blueprints/serializer/index.js b/packages/-ember-data/blueprints/serializer/index.js index b610a185d1d..e75fdaa71e1 100644 --- a/packages/-ember-data/blueprints/serializer/index.js +++ b/packages/-ember-data/blueprints/serializer/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/serializer/blueprints/serializer'); +module.exports = require('@ember-data/serializer/blueprints/serializer/index'); diff --git a/packages/-ember-data/blueprints/transform-test/index.js b/packages/-ember-data/blueprints/transform-test/index.js index 8347ca35e74..975b2844461 100644 --- a/packages/-ember-data/blueprints/transform-test/index.js +++ b/packages/-ember-data/blueprints/transform-test/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/serializer/blueprints/transform-test'); +module.exports = require('@ember-data/serializer/blueprints/transform-test/index'); diff --git a/packages/-ember-data/blueprints/transform/index.js b/packages/-ember-data/blueprints/transform/index.js index 4c8fe435099..4c43dfeeb61 100644 --- a/packages/-ember-data/blueprints/transform/index.js +++ b/packages/-ember-data/blueprints/transform/index.js @@ -1,4 +1,4 @@ // Re-exporting the blueprints from the top level `ember-data` package // because blueprint discovery in ember-cli (as of 3.12) is only done // for top level packages. -module.exports = require('@ember-data/serializer/blueprints/transform'); +module.exports = require('@ember-data/serializer/blueprints/transform/index'); diff --git a/packages/-ember-data/eslint.config.mjs b/packages/-ember-data/eslint.config.mjs new file mode 100644 index 00000000000..c31d4c6be7b --- /dev/null +++ b/packages/-ember-data/eslint.config.mjs @@ -0,0 +1,41 @@ +// @ts-check +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 * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [ + '@embroider/macros', + '@ember/application/namespace', + 'ember', + '@ember/debug', + '@ember/array/proxy', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/application', + ], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + }, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + qunit.ember({ + files: ['src/test-support/**/*.{js,ts}'], + allowedImports: ['@ember/debug', '@ember/owner'], + }), +]; diff --git a/packages/-ember-data/index.js b/packages/-ember-data/index.js deleted file mode 100644 index 9269b73e218..00000000000 --- a/packages/-ember-data/index.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const merge = require('broccoli-merge-trees'); -const addonBuildConfigForDataPackage = require('@ember-data/private-build-infra/src/addon-build-config-for-data-package'); -const version = require('@ember-data/private-build-infra/src/create-version-module'); - -const addonBaseConfig = addonBuildConfigForDataPackage(require('./package.json')); - -module.exports = Object.assign({}, addonBaseConfig, { - shouldRollupPrivate: true, - externalDependenciesForPrivateModule() { - return [ - 'ember', - '@ember/application/namespace', - '@ember-data/json-api', - 'ember-data/version', - '@ember-data/store/-private', - '@ember-data/store', - '@ember-data/model', - '@ember-data/model/-private', - '@ember/array/proxy', - '@ember/object/promise-proxy-mixin', - '@ember/object/proxy', - '@ember-data/tracking', - ]; - }, - treeForAddon(tree) { - // if we don't do this we won't have a super in addonBaseConfig - // as a regex is used to decide if to add one for the method - this._originalSuper = this._super; - tree = merge([tree, version()]); - return addonBaseConfig.treeForAddon.call(this, tree); - }, -}); diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index 407860d6974..6a1d244537b 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -2,6 +2,9 @@ "name": "ember-data", "version": "4.12.8", "description": "The lightweight reactive data library for JavaScript applications", + "keywords": [ + "ember-addon" + ], "repository": { "type": "git", "url": "git+ssh://git@github.com:emberjs/data.git", @@ -12,33 +15,57 @@ "test": "tests" }, "scripts": { - "prepack": "cd ../../ && pnpm build:docs" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, - "author": "", - "license": "MIT", - "peerDependencies": { - "@ember/string": "^3.0.1 || ^4.0.0" + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2, + "app-js": { + "./initializers/ember-data.js": "./app/initializers/ember-data.js", + "./services/store.js": "./app/services/store.js", + "./transforms/date.js": "./app/transforms/date.js", + "./transforms/number.js": "./app/transforms/number.js", + "./transforms/string.js": "./app/transforms/string.js", + "./transforms/boolean.js": "./app/transforms/boolean.js" + } }, - "dependencies": { - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/debug": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "@glimmer/env": "^0.1.7", - "broccoli-merge-trees": "^4.2.0", - "ember-auto-import": "^2.6.1", - "ember-cli-babel": "^7.26.11", - "ember-inflector": "^4.0.2" + "files": [ + "ember-data-logo-dark.svg", + "ember-data-logo-light.svg", + "LICENSE.md", + "README.md", + "addon-main.cjs", + "dist", + "app", + "blueprints", + "unstable-preview-types" + ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./test-support": { + "types": "./unstable-preview-types/test-support/index.d.ts", + "default": "./dist/test-support/index.js" + }, + "./app/*": { + "default": "./app/*.js" + }, + "./blueprints/*": { + "default": "./blueprints/*.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } }, + "author": "", + "license": "MIT", "dependenciesMeta": { "@ember-data/adapter": { "injected": true @@ -58,6 +85,9 @@ "@ember-data/request": { "injected": true }, + "@ember-data/request-utils": { + "injected": true + }, "@ember-data/legacy-compat": { "injected": true }, @@ -70,30 +100,73 @@ "@ember-data/tracking": { "injected": true }, - "@ember-data/private-build-infra": { + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, + "dependencies": { + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@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", + "@ember/test-waiters": "^3.1.0", + "qunit": "^2.18.0" + }, + "peerDependenciesMeta": { + "@ember/test-helpers": { + "optional": true + }, + "@ember/test-waiters": { + "optional": true + }, + "qunit": { + "optional": true + } + }, "devDependencies": { - "@babel/core": "^7.21.4", - "@ember/string": "^4.0.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "ember-source": "~4.12.0", - "webpack": "^5.77.0" + "@types/qunit": "2.19.10", + "@ember/test-helpers": "4.0.4", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "eslint": "^9.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "vite": "^5.2.11", + "typescript": "^5.4.5", + "qunit": "^2.18.0" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, - "keywords": [ - "ember-addon" - ], - "ember-addon": {}, "ember": { "edition": "octane" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/-ember-data/src/-private/core.ts b/packages/-ember-data/src/-private/core.ts new file mode 100644 index 00000000000..54fdb8270ec --- /dev/null +++ b/packages/-ember-data/src/-private/core.ts @@ -0,0 +1,22 @@ +import Namespace from '@ember/application/namespace'; +import Ember from 'ember'; + +import VERSION from '../version'; + +export interface DS extends Namespace { + VERSION: string; + name: string; +} + +type CreateArgs = { VERSION: string; name: string }; + +export const DS = (Namespace as unknown as { create(args: CreateArgs): DS }).create({ + VERSION: VERSION, + name: 'DS', +}); + +if (Ember.libraries) { + Ember.libraries.registerCoreLibrary('Ember Data', VERSION); +} + +export default DS; diff --git a/packages/-ember-data/src/-private/index.ts b/packages/-ember-data/src/-private/index.ts new file mode 100644 index 00000000000..b857bc4ef7c --- /dev/null +++ b/packages/-ember-data/src/-private/index.ts @@ -0,0 +1,17 @@ +// public +import ArrayProxy from '@ember/array/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + +export { default as Store } from '../store'; + +export { DS } from './core'; +export { Errors } from '@ember-data/model/-private'; +export { Snapshot } from '@ember-data/legacy-compat/-private'; + +export { RecordArrayManager, coerceId } from '@ember-data/store/-private'; +export { ManyArray, PromiseManyArray } from '@ember-data/model/-private'; +export { SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; + +export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin); +export const PromiseObject = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/-ember-data/addon/adapter.ts b/packages/-ember-data/src/adapter.ts similarity index 100% rename from packages/-ember-data/addon/adapter.ts rename to packages/-ember-data/src/adapter.ts diff --git a/packages/-ember-data/addon/adapters/errors.ts b/packages/-ember-data/src/adapters/errors.ts similarity index 100% rename from packages/-ember-data/addon/adapters/errors.ts rename to packages/-ember-data/src/adapters/errors.ts diff --git a/packages/-ember-data/addon/adapters/json-api.ts b/packages/-ember-data/src/adapters/json-api.ts similarity index 100% rename from packages/-ember-data/addon/adapters/json-api.ts rename to packages/-ember-data/src/adapters/json-api.ts diff --git a/packages/-ember-data/addon/adapters/rest.ts b/packages/-ember-data/src/adapters/rest.ts similarity index 100% rename from packages/-ember-data/addon/adapters/rest.ts rename to packages/-ember-data/src/adapters/rest.ts diff --git a/packages/-ember-data/addon/attr.ts b/packages/-ember-data/src/attr.ts similarity index 100% rename from packages/-ember-data/addon/attr.ts rename to packages/-ember-data/src/attr.ts diff --git a/packages/-ember-data/src/index.ts b/packages/-ember-data/src/index.ts new file mode 100644 index 00000000000..87d64f7b851 --- /dev/null +++ b/packages/-ember-data/src/index.ts @@ -0,0 +1,303 @@ +/** +

+ +

+ +

The lightweight reactive data library for JavaScript applications

+ +--- + +Wrangle your application's data management with scalable patterns for developer productivity. + +- ⚡️ Committed to Best-In-Class Performance +- 🌲 Focused on being as svelte as possible +- 🚀 SSR Ready +- 🔜 Typescript Support +- 🐹 Built with ♥️ by [Ember](https://emberjs.com) +- ⚛️ Supports any API: `GraphQL` `JSON:API` `REST` `tRPC` ...bespoke or a mix + +### 📖 On This Page + +- [Overview](./#overview) + - [Architecture](#🪜-architecture) + - [Basic Installation](#basic-installation) + - [Advanced Installation](#advanced-installation) +- [Configuration](#configuration) + - [Deprecation Stripping](#deprecation-stripping) + - [randomUUID polyfill](#randomuuid-polyfill) + - [Removing inspector support in production](#removing-inspector-support-in-production) + - [Debugging](#debugging) + + +# Overview + +*Ember*‍**Data** is a lightweight reactive data library for JavaScript applications that provides composable primitives for ordering query/mutation/peek flows, managing network and cache, and reducing data for presentation. + +## 🪜 Architecture + +The core of *Ember*‍**Data** is the `Store`, which coordinates interaction between your application, the `Cache`, and sources of data (such as your `API` or a local persistence layer). +Optionally, the Store can be configured to hydrate the response data into rich presentation classes. + +*Ember*‍**Data** is both resource centric and document centric in it's approach to caching, requesting and presenting data. Your application's configuration and usage drives which is important and when. + +The `Store` is a **coordinator**. When using a `Store` you configure what cache to use, how cache data should be presented to the UI, and where it should look for requested data when it is not available in the cache. + +This coordination is handled opaquely to the nature of the requests issued and the format of the data being handled. This approach gives applications broad flexibility to configure *Ember*‍**Data** to best suite their needs. This makes *Ember*‍**Data** a powerful solution for applications regardless of their size and complexity. + +*Ember*‍**Data** is designed to scale, with a religious focus on performance and asset-size to keep its footprint small but speedy while still being able to handle large complex APIs in huge data-driven applications with no additional code and no added application complexity. It's goal is to prevent applications from writing code to manage data that is difficult to maintain or reason about. + +*Ember*‍**Data**'s power comes not from specific features, data formats, or adherence to specific API specs such as `JSON:API` `trpc` or `GraphQL`, but from solid conventions around requesting and mutating data developed over decades of experience scaling developer productivity. + +## Basic Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add ember-data +``` + +`ember-data` is installed by default for new applications generated with `ember-cli`. You can check what version is installed by looking in the `devDependencies` hash of your project's [package.json](https://docs.npmjs.com/cli/v8/configuring-npm/package-json) file. + +If you have generated a new `Ember` application using `ember-cli` but do +not wish to use `ember-data`, remove `ember-data` from your project's `package.json` file and run your package manager's install command to update your lockfile. + +## Advanced Installation + +*Ember*‍**Data** is organized into primitives that compose together via public APIs. + +- [@ember-data/store](/ember-data/release/modules/@ember-data%2Fstore) is the core and handles coordination +- [@ember-data/json-api](/ember-data/release/modules/@ember-data%2Fjson-api) provides a resource cache for JSON:API structured data. It integrates with the store via the hook `createCache` +- [@ember-data/model](/ember-data/release/modules/@ember-data%2Fmodel) is a presentation layer, it integrates with the store via the hooks `instantiateRecord` and `teardownRecord`. +- [@ember-data/adapter](/ember-data/release/modules/@ember-data%2Fadapter) provides various network API integrations for APIS built over specific REST or JSON:API conventions. +- [@ember-data/serializer](/ember-data/release/modules/@ember-data%2Fserializer) pairs with `@ember-data/adapter` to normalize and serialize data to and from an API format into the `JSON:API` format understood by `@ember-data/json-api`. +- [@ember-data/debug](/ember-data/release/modules/@ember-data%2Fdebug) provides debugging support for the `ember-inspector`. +- **ember-data** is a "meta" package which bundles all of these together for convenience + +The packages interop with each other through well defined public API boundaries. The core +of the library is the store provided by `@ember-data/store`, while each of the other libraries plugs into the store when installed. Because these packages interop via fully +public APIs, other libraries or applications may provide their own implementations. For instance, [ember-m3](https://github.com/hjdivad/ember-m3) is a commonly used presentation and cache implementation suitable for complex resource objects and graphs. + +## Configuration + +### Deprecation Stripping + +*Ember*‍**Data** allows users to opt-in and remove code that exists to support deprecated behaviors. + +If your app has resolved all deprecations present in a given version, you may specify that version as your "compatibility" version to remove the code that supported the deprecated behavior from your app. + +```ts +let app = new EmberApp(defaults, { + emberData: { + compatWith: '4.8', + }, +}); +``` + +- [Full Documentation](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fdeprecations) + +### randomUUID polyfill + +*Ember*‍**Data** uses `UUID V4` by default to generate identifiers for new data created on the client. Identifier generation is configurable, but we also for convenience will polyfill +the necessary feature if your browser support or deployment environment demands it. To +activate this polyfill: + +```ts +let app = new EmberApp(defaults, { + emberData: { + polyfillUUID: true + }, +}); +``` + +### removing inspector support in production + +If you do not with to ship inspector support in your production application, you can specify +that all support for it should be stripped from the build. + +```ts +let app = new EmberApp(defaults, { + emberData: { + includeDataAdapterInProduction: false + } +}); +``` + +- [Full Documentation](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fdebug) + +### Debugging + +Many portions of the internals are helpfully instrumented with logging that can be activated +at build time. This instrumentation is always removed from production builds or any builds +that has not explicitly activated it. To activate it set the appropriate flag to `true`. + +```ts + let app = new EmberApp(defaults, { + emberData: { + debug: { + LOG_PAYLOADS: false, // data store received to update cache with + LOG_OPERATIONS: false, // updates to cache remote state + LOG_MUTATIONS: false, // updates to cache local state + LOG_NOTIFICATIONS: false, + LOG_REQUESTS: false, // log Requests issued via the request manager + LOG_REQUEST_STATUS: false, + LOG_IDENTIFIERS: false, + LOG_GRAPH: false, + LOG_INSTANCE_CACHE: false, + } + } + }); + ``` + + @module ember-data-overview + @main ember-data-overview +*/ +import { deprecate } from '@ember/debug'; + +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; + +import Adapter, { BuildURLMixin } from '@ember-data/adapter'; +import AdapterError, { + AbortError, + ConflictError, + ForbiddenError, + InvalidError, + NotFoundError, + ServerError, + TimeoutError, + UnauthorizedError, +} from '@ember-data/adapter/error'; +import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import RESTAdapter from '@ember-data/adapter/rest'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import Serializer from '@ember-data/serializer'; +import JSONSerializer from '@ember-data/serializer/json'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import RESTSerializer, { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; +import Transform, { + BooleanTransform, + DateTransform, + NumberTransform, + StringTransform, +} from '@ember-data/serializer/transform'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + +import { + DS, + Errors, + ManyArray, + PromiseArray, + PromiseManyArray, + PromiseObject, + RecordArrayManager, + Snapshot, + Store, +} from './-private/index'; +import setupContainer from './setup-container'; + +deprecate( + 'Importing from `ember-data` is deprecated. Please import from the appropriate `@ember-data/*` 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', + }, + } +); + +interface DSLibrary extends DS { + Store: typeof Store; + PromiseArray: typeof PromiseArray; + PromiseObject: typeof PromiseObject; + PromiseManyArray: typeof PromiseManyArray; + Model: typeof Model; + attr: typeof attr; + Errors: typeof Errors; + Snapshot: typeof Snapshot; + Adapter: typeof Adapter; + AdapterError: typeof AdapterError; + InvalidError: typeof InvalidError; + TimeoutError: typeof TimeoutError; + AbortError: typeof AbortError; + UnauthorizedError: typeof UnauthorizedError; + ForbiddenError: typeof ForbiddenError; + NotFoundError: typeof NotFoundError; + ConflictError: typeof ConflictError; + ServerError: typeof ServerError; + Serializer: typeof Serializer; + DebugAdapter?: typeof import('@ember-data/debug').default; + ManyArray: typeof ManyArray; + RecordArrayManager: typeof RecordArrayManager; + RESTAdapter: typeof RESTAdapter; + BuildURLMixin: typeof BuildURLMixin; + RESTSerializer: typeof RESTSerializer; + JSONSerializer: typeof JSONSerializer; + JSONAPIAdapter: typeof JSONAPIAdapter; + JSONAPISerializer: typeof JSONAPISerializer; + Transform: typeof Transform; + DateTransform: typeof DateTransform; + StringTransform: typeof StringTransform; + NumberTransform: typeof NumberTransform; + BooleanTransform: typeof BooleanTransform; + EmbeddedRecordsMixin: typeof EmbeddedRecordsMixin; + belongsTo: typeof belongsTo; + hasMany: typeof hasMany; + _setupContainer: typeof setupContainer; +} + +function upgradeDS(obj: unknown): asserts obj is DSLibrary {} + +upgradeDS(DS); + +DS.Store = Store; +DS.PromiseArray = PromiseArray; +DS.PromiseObject = PromiseObject; +DS.PromiseManyArray = PromiseManyArray; +DS.Model = Model; +DS.attr = attr; +DS.Errors = Errors; +DS.Snapshot = Snapshot; +DS.Adapter = Adapter; +DS.AdapterError = AdapterError; +DS.InvalidError = InvalidError; +DS.TimeoutError = TimeoutError; +DS.AbortError = AbortError; +DS.UnauthorizedError = UnauthorizedError; +DS.ForbiddenError = ForbiddenError; +DS.NotFoundError = NotFoundError; +DS.ConflictError = ConflictError; +DS.ServerError = ServerError; +DS.Serializer = Serializer; + +if (macroCondition(dependencySatisfies('@ember-data/debug', '*'))) { + DS.DebugAdapter = importSync('@ember-data/debug') as typeof import('@ember-data/debug').default; +} + +DS.ManyArray = ManyArray; +DS.RecordArrayManager = RecordArrayManager; +DS.RESTAdapter = RESTAdapter; +DS.BuildURLMixin = BuildURLMixin; +DS.RESTSerializer = RESTSerializer; +DS.JSONSerializer = JSONSerializer; +DS.JSONAPIAdapter = JSONAPIAdapter; +DS.JSONAPISerializer = JSONAPISerializer; +DS.Transform = Transform; +DS.DateTransform = DateTransform; +DS.StringTransform = StringTransform; +DS.NumberTransform = NumberTransform; +DS.BooleanTransform = BooleanTransform; +DS.EmbeddedRecordsMixin = EmbeddedRecordsMixin; +DS.belongsTo = belongsTo; +DS.hasMany = hasMany; +DS._setupContainer = setupContainer; + +export default DS; diff --git a/packages/-ember-data/addon/model.ts b/packages/-ember-data/src/model.ts similarity index 100% rename from packages/-ember-data/addon/model.ts rename to packages/-ember-data/src/model.ts diff --git a/packages/-ember-data/addon/relationships.ts b/packages/-ember-data/src/relationships.ts similarity index 100% rename from packages/-ember-data/addon/relationships.ts rename to packages/-ember-data/src/relationships.ts diff --git a/packages/-ember-data/addon/serializer.ts b/packages/-ember-data/src/serializer.ts similarity index 100% rename from packages/-ember-data/addon/serializer.ts rename to packages/-ember-data/src/serializer.ts diff --git a/packages/-ember-data/addon/serializers/embedded-records-mixin.ts b/packages/-ember-data/src/serializers/embedded-records-mixin.ts similarity index 100% rename from packages/-ember-data/addon/serializers/embedded-records-mixin.ts rename to packages/-ember-data/src/serializers/embedded-records-mixin.ts diff --git a/packages/-ember-data/addon/serializers/json-api.ts b/packages/-ember-data/src/serializers/json-api.ts similarity index 100% rename from packages/-ember-data/addon/serializers/json-api.ts rename to packages/-ember-data/src/serializers/json-api.ts diff --git a/packages/-ember-data/addon/serializers/json.ts b/packages/-ember-data/src/serializers/json.ts similarity index 100% rename from packages/-ember-data/addon/serializers/json.ts rename to packages/-ember-data/src/serializers/json.ts diff --git a/packages/-ember-data/addon/serializers/rest.ts b/packages/-ember-data/src/serializers/rest.ts similarity index 100% rename from packages/-ember-data/addon/serializers/rest.ts rename to packages/-ember-data/src/serializers/rest.ts diff --git a/packages/-ember-data/addon/setup-container.ts b/packages/-ember-data/src/setup-container.ts similarity index 100% rename from packages/-ember-data/addon/setup-container.ts rename to packages/-ember-data/src/setup-container.ts diff --git a/packages/-ember-data/src/store.ts b/packages/-ember-data/src/store.ts new file mode 100644 index 00000000000..d750281c67e --- /dev/null +++ b/packages/-ember-data/src/store.ts @@ -0,0 +1,82 @@ +import JSONAPICache from '@ember-data/json-api'; +import { + adapterFor, + cleanup, + LegacyNetworkHandler, + normalize, + pushPayload, + serializeRecord, + serializerFor, +} from '@ember-data/legacy-compat'; +import type { FetchManager } from '@ember-data/legacy-compat/-private'; +import type Model from '@ember-data/model'; +import type { ModelStore } from '@ember-data/model/-private'; +import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember-data/model/hooks'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import BaseStore, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; + +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; + + constructor(args?: Record) { + super(args); + + if (!hasRequestManager(this)) { + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + } + this.requestManager.useCache(CacheHandler); + } + + createSchemaService(): SchemaService { + return buildSchema(this); + } + + createCache(storeWrapper: CacheCapabilitiesManager): Cache { + return new JSONAPICache(storeWrapper); + } + + instantiateRecord( + this: ModelStore, + identifier: StableRecordIdentifier, + createRecordArgs: Record + ): Model { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + teardownRecord(record: Model): void { + teardownRecord.call(this, record); + } + + modelFor(type: TypeFromInstance): ModelSchema; + modelFor(type: string): ModelSchema; + modelFor(type: string): ModelSchema { + return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type); + } + + adapterFor = adapterFor; + serializerFor = serializerFor; + pushPayload = pushPayload; + normalize = normalize; + serializeRecord = serializeRecord; + + destroy() { + cleanup.call(this); + super.destroy(); + } +} diff --git a/packages/-ember-data/src/test-support/index.ts b/packages/-ember-data/src/test-support/index.ts new file mode 100644 index 00000000000..b4ec66d35d3 --- /dev/null +++ b/packages/-ember-data/src/test-support/index.ts @@ -0,0 +1,33 @@ +import type Owner from '@ember/owner'; +import { render as renderTemplate, settled } from '@ember/test-helpers'; + +import * as QUnit from 'qunit'; + +import type Store from '@ember-data/store'; +import { PRODUCTION } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; + +/* + Temporary replacement for the render test helper + which we will deprecate in EmberData 5.0, this allows + an app to incrementally migrate to tests that render async + relationships in stages with potential for tests in between. +*/ +export async function render(template: object) { + await renderTemplate(template); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const owner = QUnit.config.current.testEnvironment.owner as Owner; + const pending = (owner.lookup('service:store') as Store)._getAllPending(); + + // this should only be necessary in production tests + // where @ember/test-waiters is deactivated :() + if (PRODUCTION) { + assert( + `No pending requests exist in this test, use \`import { render } from '@ember/test-helpers';\``, + pending?.length + ); + + await pending; + await settled(); + } +} diff --git a/packages/-ember-data/addon/transform.ts b/packages/-ember-data/src/transform.ts similarity index 100% rename from packages/-ember-data/addon/transform.ts rename to packages/-ember-data/src/transform.ts diff --git a/packages/-ember-data/src/version.ts b/packages/-ember-data/src/version.ts new file mode 100644 index 00000000000..55f11914951 --- /dev/null +++ b/packages/-ember-data/src/version.ts @@ -0,0 +1 @@ +export { version as default } from '../package.json'; diff --git a/packages/-ember-data/tsconfig.json b/packages/-ember-data/tsconfig.json new file mode 100644 index 00000000000..a7834450ba3 --- /dev/null +++ b/packages/-ember-data/tsconfig.json @@ -0,0 +1,101 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/adapter": ["../adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../adapter/unstable-preview-types/*"], + "@ember-data/debug": ["../debug/unstable-preview-types"], + "@ember-data/debug/*": ["../debug/unstable-preview-types/*"], + "@ember-data/graph": ["../graph/unstable-preview-types"], + "@ember-data/graph/*": ["../graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../model/unstable-preview-types"], + "@ember-data/model/*": ["../model/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../serializer/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../adapter" + }, + { + "path": "../graph" + }, + { + "path": "../json-api" + }, + { + "path": "../legacy-compat" + }, + { + "path": "../model" + }, + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../serializer" + }, + { + "path": "../store" + }, + { + "path": "../debug" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/-ember-data/vite.config.mjs b/packages/-ember-data/vite.config.mjs new file mode 100644 index 00000000000..5fb0e015227 --- /dev/null +++ b/packages/-ember-data/vite.config.mjs @@ -0,0 +1,25 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember/application/namespace', + 'ember', + '@ember/debug', + '@ember/array/proxy', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/application', + '@ember/owner', + 'qunit', + '@ember/test-waiters', + '@ember/test-helpers', +]; + +export const entryPoints = ['./src/**/*.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/active-record/CHANGELOG.md b/packages/active-record/CHANGELOG.md new file mode 100644 index 00000000000..b58d2b12aae --- /dev/null +++ b/packages/active-record/CHANGELOG.md @@ -0,0 +1,59 @@ +# @ember-data/active-record Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@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)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#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)) +* [#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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@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)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/private-build-infra/LICENSE.md b/packages/active-record/LICENSE.md similarity index 100% rename from packages/private-build-infra/LICENSE.md rename to packages/active-record/LICENSE.md diff --git a/packages/active-record/README.md b/packages/active-record/README.md new file mode 100644 index 00000000000..1190001b229 --- /dev/null +++ b/packages/active-record/README.md @@ -0,0 +1,78 @@ +

+ + +

+ +

Elegantly composable. Made for ActiveRecord

+ +This package provides utilities for working with **Active**Record APIs with [*Ember***Data**](https://github.com/emberjs/data/). + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/active-record +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/active-record/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/active-record/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/active-record/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/active-record/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/active-record/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + +## Getting Started + +If this package is how you are first learning about EmberData, we recommend starting with learning about the [Store](https://github.com/emberjs/data/blob/main/packages/store/README.md) and [Requests](https://github.com/emberjs/data/blob/main/packages/request/README.md) + +## Request Builders + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/active-record/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/* + => { + url: 'https://api.example.com/v1/ember_developers/1?include=friends,pets', + method: 'GET', + headers: , // 'Content-Type': 'application/json;charset=utf-8' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +*/ +``` + +Request builder output may be used with either `requestManager.request` or `store.request`. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common ActiveRecord format (underscored pluralized resource types). + +### Available Builders + +- [createRecord]() +- [deleteRecord]() +- [findRecord]() +- [query]() +- [updateRecord]() diff --git a/packages/active-record/addon-main.cjs b/packages/active-record/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/active-record/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/active-record/babel.config.mjs b/packages/active-record/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/active-record/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/active-record/ember-data-logo-dark.svg b/packages/active-record/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/active-record/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/active-record/ember-data-logo-light.svg b/packages/active-record/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/active-record/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/active-record/eslint.config.mjs b/packages/active-record/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/active-record/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/active-record/package.json b/packages/active-record/package.json new file mode 100644 index 00000000000..c851420fdae --- /dev/null +++ b/packages/active-record/package.json @@ -0,0 +1,98 @@ +{ + "name": "@ember-data/active-record", + "description": "ActiveRecord Format Support for EmberData", + "version": "4.12.8", + "private": false, + "license": "MIT", + "author": "Chris Thoburn ", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/active-record" + }, + "homepage": "https://github.com/emberjs/data", + "bugs": "https://github.com/emberjs/data/issues", + "engines": { + "node": ">= 18.20.4" + }, + "keywords": [ + "ember-addon" + ], + "files": [ + "addon-main.cjs", + "dist", + "README.md", + "LICENSE.md", + "ember-data-logo-dark.svg", + "ember-data-logo-light.svg", + "unstable-preview-types" + ], + "exports": { + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 + }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "peerDependencies": { + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "^4.12.0 || ^5.0.0", + "@warp-drive/core-types": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@glimmer/component": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "vite": "^5.2.11", + "typescript": "^5.4.5" + }, + "dependenciesMeta": { + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + } + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/active-record/src/-private/builders/-utils.ts b/packages/active-record/src/-private/builders/-utils.ts new file mode 100644 index 00000000000..3d711858753 --- /dev/null +++ b/packages/active-record/src/-private/builders/-utils.ts @@ -0,0 +1,25 @@ +import type { UrlOptions } from '@ember-data/request-utils'; +import type { CacheOptions, ConstrainedRequestOptions } from '@warp-drive/core-types/request'; + +export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void { + if ('host' in options) { + urlOptions.host = options.host; + } + if ('namespace' in options) { + urlOptions.namespace = options.namespace; + } + if ('resourcePath' in options) { + urlOptions.resourcePath = options.resourcePath; + } +} + +export function extractCacheOptions(options: ConstrainedRequestOptions) { + const cacheOptions: CacheOptions = {}; + if ('reload' in options) { + cacheOptions.reload = options.reload; + } + if ('backgroundReload' in options) { + cacheOptions.backgroundReload = options.backgroundReload; + } + return cacheOptions; +} diff --git a/packages/active-record/src/-private/builders/find-record.ts b/packages/active-record/src/-private/builders/find-record.ts new file mode 100644 index 00000000000..90f6109de1f --- /dev/null +++ b/packages/active-record/src/-private/builders/find-record.ts @@ -0,0 +1,120 @@ +/** + * @module @ember-data/active-record/request + */ +import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import { pluralize, underscore } from '@ember-data/request-utils/string'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { + FindRecordOptions, + FindRecordRequestOptions, + RemotelyAccessibleIdentifier, +} from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; + +/** + * Builds request options to fetch a single resource by a known id or identifier + * configured for the url and header expectations of most ActiveRecord APIs. + * + * **Basic Usage** + * + * ```ts + * import { findRecord } from '@ember-data/active-record/request'; + * + * const data = await store.request(findRecord('person', '1')); + * ``` + * + * **With Options** + * + * ```ts + * import { findRecord } from '@ember-data/active-record/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **With an Identifier** + * + * ```ts + * import { findRecord } from '@ember-data/active-record/request'; + * + * const options = findRecord({ type: 'person', id: '1' }, { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing and underscoring the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { findRecord } from '@ember-data/active-record/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }, { namespace: 'api/v2' }); + * const data = await store.request(options); + * ``` + * + * @method findRecord + * @public + * @static + * @for @ember-data/active-record/request + * @param identifier + * @param options + */ +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord( + identifier: RemotelyAccessibleIdentifier, + options?: FindRecordOptions +): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; +export function findRecord( + arg1: string | RemotelyAccessibleIdentifier, + arg2: string | FindRecordOptions | undefined, + arg3?: FindRecordOptions +): FindRecordRequestOptions { + const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1; + const options = ((typeof arg1 === 'string' ? arg3 : arg2) || {}) as FindRecordOptions; + const cacheOptions = extractCacheOptions(options); + const urlOptions: FindRecordUrlOptions = { + identifier, + op: 'findRecord', + resourcePath: pluralize(underscore(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url: options.include?.length + ? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}` + : url, + method: 'GET', + headers, + cacheOptions, + op: 'findRecord', + records: [identifier], + }; +} diff --git a/packages/active-record/src/-private/builders/query.ts b/packages/active-record/src/-private/builders/query.ts new file mode 100644 index 00000000000..6719e8dfdff --- /dev/null +++ b/packages/active-record/src/-private/builders/query.ts @@ -0,0 +1,102 @@ +/** + * @module @ember-data/active-record/request + */ +import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; +import { pluralize, underscore } from '@ember-data/request-utils/string'; +import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { ConstrainedRequestOptions, QueryRequestOptions } from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +/** + * Builds request options to query for resources, usually by a primary + * type, configured for the url and header expectations of most ActiveRecord APIs. + * + * **Basic Usage** + * + * ```ts + * import { query } from '@ember-data/active-record/request'; + * + * const data = await store.request(query('person')); + * ``` + * + * **With Query Params** + * + * ```ts + * import { query } from '@ember-data/active-record/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing and underscoring the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { query } from '@ember-data/active-record/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }, { reload: true }); + * const data = await store.request(options); + * ``` + * + * @method query + * @public + * @static + * @for @ember-data/active-record/request + * @param identifier + * @param query + * @param options + */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query: QueryParamsSource = {}, + options: ConstrainedRequestOptions = {} +): QueryRequestOptions { + const cacheOptions = extractCacheOptions(options); + const urlOptions: QueryUrlOptions = { + identifier: { type }, + op: 'query', + resourcePath: pluralize(underscore(type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + const queryString = buildQueryParams(query, options.urlParamsSettings); + + return { + url: queryString ? `${url}?${queryString}` : url, + method: 'GET', + headers, + cacheOptions, + op: 'query', + }; +} diff --git a/packages/active-record/src/-private/builders/save-record.ts b/packages/active-record/src/-private/builders/save-record.ts new file mode 100644 index 00000000000..5778dfff65e --- /dev/null +++ b/packages/active-record/src/-private/builders/save-record.ts @@ -0,0 +1,262 @@ +import { + buildBaseURL, + type CreateRecordUrlOptions, + type DeleteRecordUrlOptions, + type UpdateRecordUrlOptions, +} from '@ember-data/request-utils'; +import { pluralize, underscore } from '@ember-data/request-utils/string'; +import { recordIdentifierFor } from '@ember-data/store'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { + ConstrainedRequestOptions, + CreateRequestOptions, + DeleteRequestOptions, + UpdateRequestOptions, +} from '@warp-drive/core-types/request'; + +import { copyForwardUrlOptions } from './-utils'; + +function isExisting(identifier: StableRecordIdentifier): identifier is StableExistingRecordIdentifier { + return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null; +} + +/** + * Builds request options to delete record for resources, + * configured for the url, method and header expectations of ActiveRecord APIs. + * + * **Basic Usage** + * + * ```ts + * import { deleteRecord } from '@ember-data/active-record/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const data = await store.request(deleteRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { deleteRecord } from '@ember-data/active-record/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const options = deleteRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method deleteRecord + * @public + * @static + * @for @ember-data/active-record/request + * @param record + * @param options + */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot delete a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: DeleteRecordUrlOptions = { + identifier: identifier, + op: 'deleteRecord', + resourcePath: pluralize(underscore(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: 'DELETE', + headers, + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to create new record for resources, + * configured for the url, method and header expectations of most ActiveRecord APIs. + * + * **Basic Usage** + * + * ```ts + * import { createRecord } from '@ember-data/active-record/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const data = await store.request(createRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { createRecord } from '@ember-data/active-record/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const options = createRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method createRecord + * @public + * @static + * @for @ember-data/active-record/request + * @param record + * @param options + */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + + const urlOptions: CreateRecordUrlOptions = { + identifier: identifier, + op: 'createRecord', + resourcePath: pluralize(underscore(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: 'POST', + headers, + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to update existing record for resources, + * configured for the url, method and header expectations of most ActiveRecord APIs. + * + * **Basic Usage** + * + * ```ts + * import { updateRecord } from '@ember-data/active-record/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const data = await store.request(updateRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `patch` - Allows caller to specify whether to use a PATCH request instead of a PUT request, defaults to `false`. + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { updateRecord } from '@ember-data/active-record/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const options = updateRecord(person, { patch: true }); + * const data = await store.request(options); + * ``` + * + * @method updateRecord + * @public + * @static + * @for @ember-data/active-record/request + * @param record + * @param options + */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options: ConstrainedRequestOptions & { patch?: boolean } = {} +): UpdateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot update a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: UpdateRecordUrlOptions = { + identifier: identifier, + op: 'updateRecord', + resourcePath: pluralize(underscore(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: options.patch ? 'PATCH' : 'PUT', + headers, + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} diff --git a/packages/unpublished-test-infra/tests/dummy/public/.gitkeep b/packages/active-record/src/.gitkeep similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/public/.gitkeep rename to packages/active-record/src/.gitkeep diff --git a/packages/active-record/src/request.ts b/packages/active-record/src/request.ts new file mode 100644 index 00000000000..1f6c37c6b51 --- /dev/null +++ b/packages/active-record/src/request.ts @@ -0,0 +1,66 @@ +/** + *

+ +

+ +This package provides utilities for working with [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html#convention-over-configuration-in-active-record) APIs with [*Ember***Data**](https://github.com/emberjs/data/). + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/active-record +``` + +## Usage + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/active-record/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/\* + => { + url: 'https://api.example.com/v1/ember_developers/1?include=friends,pets', + method: 'GET', + headers: , // 'Content-Type': 'application/json;charset=utf-8' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +*\/ +``` + +Request builder output may be used with either `requestManager.request` or `store.request`. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common ActiveRecord format (underscored pluralized resource types). + +### Available Builders + +- [createRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Factive-record/createRecord) +- [deleteRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Factive-record/deleteRecord) +- [findRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Factive-record/findRecord) +- [query](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Factive-record/query) +- [updateRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Factive-record/updateRecord) + + * + * @module @ember-data/active-record/request + * @main @ember-data/active-record/request + */ +export { findRecord } from './-private/builders/find-record'; +export { query } from './-private/builders/query'; +export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record'; diff --git a/packages/active-record/tsconfig.json b/packages/active-record/tsconfig.json new file mode 100644 index 00000000000..eb3e30415db --- /dev/null +++ b/packages/active-record/tsconfig.json @@ -0,0 +1,66 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/active-record/vite.config.mjs b/packages/active-record/vite.config.mjs new file mode 100644 index 00000000000..7840d9c894a --- /dev/null +++ b/packages/active-record/vite.config.mjs @@ -0,0 +1,12 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = []; +export const entryPoints = ['./src/request.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/adapter/CHANGELOG.md b/packages/adapter/CHANGELOG.md new file mode 100644 index 00000000000..a3849246487 --- /dev/null +++ b/packages/adapter/CHANGELOG.md @@ -0,0 +1,79 @@ +# @ember-data/adapter Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@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)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +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)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#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)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@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)) +* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#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)) +* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/adapter/README.md b/packages/adapter/README.md index 43b9162f6c9..90d98d724f4 100644 --- a/packages/adapter/README.md +++ b/packages/adapter/README.md @@ -31,11 +31,21 @@ If installing `@ember-data/` packages individually install using your javascript pnpm add @ember-data/adapter ``` +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/adapter/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/adapter/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/adapter/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/adapter/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/adapter/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + ## 🚀 Setup If using `ember-data` no additional setup is necesssary. -> **Note** When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below +> **Note** +> When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below > configuration is handled for you automatically. To use legacy adapters you will need to have installed and configured the LegacyNetworkHandler from [@ember-data/legacy-compat](https://github.com/emberjs/data/blob/main/packages/-ember-data) diff --git a/packages/adapter/addon-main.cjs b/packages/adapter/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/adapter/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/adapter/addon-main.js b/packages/adapter/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/adapter/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/adapter/babel.config.js b/packages/adapter/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/adapter/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/adapter/babel.config.mjs b/packages/adapter/babel.config.mjs new file mode 100644 index 00000000000..c23b859273f --- /dev/null +++ b/packages/adapter/babel.config.mjs @@ -0,0 +1,12 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ['module:decorator-transforms', { runtime: { import: 'decorator-transforms/runtime' } }], + ], +}; diff --git a/packages/adapter/blueprints/adapter-test/index.js b/packages/adapter/blueprints/adapter-test/index.js index 3181a7f245f..309f6ce25ca 100644 --- a/packages/adapter/blueprints/adapter-test/index.js +++ b/packages/adapter/blueprints/adapter-test/index.js @@ -1,15 +1,15 @@ const path = require('path'); const testInfo = require('ember-cli-test-info'); -const useTestFrameworkDetector = require('@ember-data/private-build-infra/src/utilities/test-framework-detector'); -const modulePrefixForProject = require('@ember-data/private-build-infra/src/utilities/module-prefix-for-project'); +const { dasherize } = require('ember-cli-string-utils'); -module.exports = useTestFrameworkDetector({ - description: 'Generates an ember-data adapter unit test', +module.exports = { + description: 'Generates an EmberData adapter unit test', + supportsAddon() { return false; }, root: __dirname, - fileMapTokens(options) { + fileMapTokens() { return { __root__() { return 'tests'; @@ -21,9 +21,15 @@ module.exports = useTestFrameworkDetector({ }, locals(options) { + const modulePrefix = dasherize(options.project.config().modulePrefix); return { friendlyTestDescription: testInfo.description(options.entity.name, 'Unit', 'Adapter'), - modulePrefix: modulePrefixForProject(options.project), + modulePrefix, }; }, -}); + + filesPath() { + return path.join(__dirname, 'qunit-files') + } +}; + diff --git a/packages/adapter/blueprints/adapter-test/mocha-files/__root__/__path__/__test__.js b/packages/adapter/blueprints/adapter-test/mocha-files/__root__/__path__/__test__.js deleted file mode 100644 index 9ba21b6b313..00000000000 --- a/packages/adapter/blueprints/adapter-test/mocha-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,17 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'ember-mocha'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest('adapter:<%= dasherizedModuleName %>', { - // Specify the other units that are required for this test. - // needs: ['serializer:foo'] - }); - - // Replace this with your real tests. - it('exists', function () { - let adapter = this.subject(); - expect(adapter).to.be.ok; - }); -}); diff --git a/packages/adapter/blueprints/adapter-test/mocha-rfc-232-files/__root__/__path__/__test__.js b/packages/adapter/blueprints/adapter-test/mocha-rfc-232-files/__root__/__path__/__test__.js deleted file mode 100644 index c4c22408fb9..00000000000 --- a/packages/adapter/blueprints/adapter-test/mocha-rfc-232-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from '<%= modulePrefix %>/tests/helpers'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let adapter = this.owner.lookup('adapter:<%= dasherizedModuleName %>'); - expect(adapter).to.be.ok; - }); -}); diff --git a/packages/adapter/blueprints/adapter-test/qunit-files/__root__/__path__/__test__.js b/packages/adapter/blueprints/adapter-test/qunit-files/__root__/__path__/__test__.js index 5d9889eb364..60dfe272bdb 100644 --- a/packages/adapter/blueprints/adapter-test/qunit-files/__root__/__path__/__test__.js +++ b/packages/adapter/blueprints/adapter-test/qunit-files/__root__/__path__/__test__.js @@ -1,13 +1,12 @@ -import { module, test } from 'qunit'; - import { setupTest } from '<%= modulePrefix %>/tests/helpers'; +import { module, test } from 'qunit'; module('<%= friendlyTestDescription %>', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let adapter = this.owner.lookup('adapter:<%= dasherizedModuleName %>'); - assert.ok(adapter); + const adapter = this.owner.lookup('adapter:<%= dasherizedModuleName %>'); + assert.ok(adapter, 'adapter exists'); }); }); diff --git a/packages/adapter/blueprints/adapter/index.js b/packages/adapter/blueprints/adapter/index.js index 77ab1bcb103..75dd0e42ad7 100644 --- a/packages/adapter/blueprints/adapter/index.js +++ b/packages/adapter/blueprints/adapter/index.js @@ -1,14 +1,80 @@ -const extendFromApplicationEntity = require('@ember-data/private-build-infra/src/utilities/extend-from-application-entity'); -const useEditionDetector = require('@ember-data/private-build-infra/src/utilities/edition-detector'); +const path = require('path'); +const fs = require('fs'); -module.exports = useEditionDetector({ - description: 'Generates an ember-data adapter.', +const stringUtil = require('ember-cli-string-utils'); +const pathUtil = require('ember-cli-path-utils'); + +const { has } = require('@ember/edition-utils'); + +module.exports = { + description: 'Generates an ember-data Adapter.', availableOptions: [{ name: 'base-class', type: String }], root: __dirname, + filesPath() { + let hasOctane = has('octane'); + if (hasOctane && process.env.EMBER_EDITION === 'classic') { + hasOctane = false; //forcible override + } + let rootPath = hasOctane ? 'native-files' : 'files'; + return path.join(__dirname, rootPath); + }, + locals(options) { return extendFromApplicationEntity('adapter', 'JSONAPIAdapter', options); }, -}); +}; + +function extendFromApplicationEntity(type, baseClass, options) { + let isAddon = options.inRepoAddon || options.project.isEmberCLIAddon(); + + let entityName = options.entity.name; + let relativePath = pathUtil.getRelativePath(options.entity.name); + + if (options.pod && options.podPath) { + relativePath = pathUtil.getRelativePath(options.podPath + options.entity.name); + } + + let applicationEntityPath = path.join(options.project.root, 'app', `${type}s`, 'application.js'); + + let hasApplicationEntity = fs.existsSync(applicationEntityPath); + if (!isAddon && !options.baseClass && entityName !== 'application' && hasApplicationEntity) { + options.baseClass = 'application'; + } + + if (options.baseClass === entityName) { + throw new Error( + stringUtil.classify(type) + + 's cannot extend from themself. To resolve this, remove the `--base-class` option or change to a different base-class.' + ); + } + + let importStatement; + + if (options.baseClass) { + let baseClassPath = options.baseClass; + baseClass = stringUtil.classify(baseClassPath.replace('/', '-')); + baseClass = baseClass + stringUtil.classify(type); + + importStatement = `import ${baseClass} from '${relativePath}${baseClassPath}';`; + } else { + let baseClassPath = `@ember-data/${type}`; + + if (baseClass.startsWith('JSONAPI')) { + baseClassPath += '/json-api'; + } + + if (baseClass.startsWith('REST')) { + baseClassPath += '/rest'; + } + + importStatement = `import ${baseClass} from '${baseClassPath}';`; + } + + return { + importStatement, + baseClass, + }; +} diff --git a/packages/adapter/eslint.config.mjs b/packages/adapter/eslint.config.mjs new file mode 100644 index 00000000000..cc255e6f85c --- /dev/null +++ b/packages/adapter/eslint.config.mjs @@ -0,0 +1,30 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['src'], + allowedImports: ['@ember/object', '@ember/application', '@ember/service', '@ember/debug', '@ember/object/mixin'], + }), + + // browser (ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['src'], + allowedImports: ['@ember/object', '@ember/application', '@ember/service', '@ember/debug', '@ember/object/mixin'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 851a53f03c5..90f0527529c 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -14,68 +14,110 @@ "author": "", "directories": {}, "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 }, "files": [ + "unstable-preview-types", "blueprints", - "addon-main.js", - "addon", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./blueprints/*": { + "default": "./blueprints/*.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "peerDependencies": { - "@ember-data/store": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "ember-inflector": "^4.0.2" + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@warp-drive/core-types": "workspace:*" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-test-info": "^1.0.0" + "@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", + "@ember/edition-utils": "1.2.0", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "typescript": "^5.0.3", - "walk-sync": "^3.0.0", - "webpack": "^5.77.0" + "decorator-transforms": "^2.2.2", + "@types/jquery": "^3.5.30", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/adapter/rollup.config.mjs b/packages/adapter/rollup.config.mjs deleted file mode 100644 index 450d7d02152..00000000000 --- a/packages/adapter/rollup.config.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@ember/service', - '@ember-data/store/-private', - 'require', - 'ember-inflector', - '@ember/debug', - '@ember/string', - '@ember/object', - '@ember/object/mixin', - '@ember/application', - '@glimmer/env', - '@ember/runloop', - '@ember/polyfills', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'error.js', 'json-api.js', 'rest.js', '-private.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/adapter/src/-private.ts b/packages/adapter/src/-private.ts index 4e05290b7ed..fc8d0b781a1 100644 --- a/packages/adapter/src/-private.ts +++ b/packages/adapter/src/-private.ts @@ -2,9 +2,9 @@ @module @ember-data/adapter */ -export { default as parseResponseHeaders } from './-private/utils/parse-response-headers'; +export { parseResponseHeaders } from './-private/utils/parse-response-headers'; export { determineBodyPromise } from './-private/utils/determine-body-promise'; export { serializeQueryParams } from './-private/utils/serialize-query-params'; -export { default as fetch, setupFastboot } from './-private/utils/fetch'; -export { default as BuildURLMixin } from './-private/build-url-mixin'; -export { default as serializeIntoHash } from './-private/utils/serialize-into-hash'; +export { getFetchFunction as fetch, setupFastboot } from './-private/utils/fetch'; +export { BuildURLMixin } from './-private/build-url-mixin'; +export { serializeIntoHash } from './-private/utils/serialize-into-hash'; diff --git a/packages/adapter/src/-private/build-url-mixin.ts b/packages/adapter/src/-private/build-url-mixin.ts index 0a77089f25c..8b6f64b0469 100644 --- a/packages/adapter/src/-private/build-url-mixin.ts +++ b/packages/adapter/src/-private/build-url-mixin.ts @@ -1,10 +1,7 @@ import Mixin from '@ember/object/mixin'; -import { camelize } from '@ember/string'; - -import { pluralize } from 'ember-inflector'; import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; -import type { Dict } from '@ember-data/types/q/utils'; +import { camelize, pluralize } from '@ember-data/request-utils/string'; /** @module @ember-data/adapter @@ -20,7 +17,7 @@ import type { Dict } from '@ember-data/types/q/utils'; // `interface BuildURLMixin { buildURL: typeof buildURL }` // then an extending class overwriting one of the methods will break because typescript // thinks it is a switch from an instance prop (that is a method) to an instance method. -interface BuildURLMixin { +export interface BuildURLMixin { buildURL( this: MixtBuildURLMixin, modelName: string, @@ -41,7 +38,7 @@ interface BuildURLMixin { id: null, snapshot: null, requestType: 'query', - query: Dict + query: Record ): string; buildURL( this: MixtBuildURLMixin, @@ -49,7 +46,7 @@ interface BuildURLMixin { id: null, snapshot: null, requestType: 'queryRecord', - query: Dict + query: Record ): string; buildURL( this: MixtBuildURLMixin, @@ -97,8 +94,8 @@ interface BuildURLMixin { _buildURL(this: MixtBuildURLMixin, modelName: string | null | undefined, id?: string | null): string; urlForFindRecord(this: MixtBuildURLMixin, id: string, modelName: string, snapshot: Snapshot): string; urlForFindAll(this: MixtBuildURLMixin, modelName: string, snapshots: SnapshotRecordArray): string; - urlForQueryRecord(this: MixtBuildURLMixin, query: Dict, modelName: string): string; - urlForQuery(this: MixtBuildURLMixin, query: Dict, modelName: string): string; + urlForQueryRecord(this: MixtBuildURLMixin, query: Record, modelName: string): string; + urlForQuery(this: MixtBuildURLMixin, query: Record, modelName: string): string; urlForFindMany(this: MixtBuildURLMixin, ids: string[], modelName: string, snapshots: Snapshot[]): string; urlForFindHasMany(this: MixtBuildURLMixin, id: string, modelName: string, snapshot: Snapshot): string; urlForFindBelongsTo(this: MixtBuildURLMixin, id: string, modelName: string, snapshot: Snapshot): string; @@ -112,7 +109,7 @@ interface BuildURLMixin { // prevents the final constructed object from needing to add // host and namespace which are provided by the final consuming // class to the prototype which can result in overwrite errors -interface MixtBuildURLMixin extends BuildURLMixin { +export interface MixtBuildURLMixin extends BuildURLMixin { host: string | null; namespace: string | null; } @@ -185,7 +182,7 @@ function buildURL( id: null, snapshot: null, requestType: 'query', - query: Dict + query: Record ): string; function buildURL( this: MixtBuildURLMixin, @@ -193,7 +190,7 @@ function buildURL( id: null, snapshot: null, requestType: 'queryRecord', - query: Dict + query: Record ): string; function buildURL( this: MixtBuildURLMixin, @@ -241,7 +238,7 @@ function buildURL(this: MixtBuildURLMixin, modelName: string, id: string, snapsh function buildURL( this: MixtBuildURLMixin, modelName: string, - id: string | string[] | Dict | null, + id: string | string[] | Record | null, snapshot: Snapshot | Snapshot[] | SnapshotRecordArray | null, requestType?: | 'findRecord' @@ -254,7 +251,7 @@ function buildURL( | 'createRecord' | 'updateRecord' | 'deleteRecord', - query?: Dict + query?: Record ): string { /* Switch statements in typescript don't currently narrow even when the function is implemented @@ -300,10 +297,10 @@ function buildURL( @return {String} url */ function _buildURL(this: MixtBuildURLMixin, modelName: string | null | undefined, id?: string | null): string { - let path; - let url: string[] = []; - let { host } = this; - let prefix = this.urlPrefix(); + let path: string; + const url: string[] = []; + const { host } = this; + const prefix = this.urlPrefix(); if (modelName) { path = this.pathForType(modelName); @@ -408,7 +405,7 @@ function urlForFindAll(this: MixtBuildURLMixin, modelName: string, snapshots: Sn @param {String} modelName @return {String} url */ -function urlForQuery(this: MixtBuildURLMixin, query: Dict, modelName: string): string { +function urlForQuery(this: MixtBuildURLMixin, query: Record, modelName: string): string { return this._buildURL(modelName); } @@ -434,7 +431,7 @@ function urlForQuery(this: MixtBuildURLMixin, query: Dict, modelName: s @param {String} modelName @return {String} url */ -function urlForQueryRecord(this: MixtBuildURLMixin, query: Dict, modelName: string): string { +function urlForQueryRecord(this: MixtBuildURLMixin, query: Record, modelName: string): string { return this._buildURL(modelName); } @@ -609,7 +606,8 @@ function urlForDeleteRecord(this: MixtBuildURLMixin, id: string, modelName: stri @return {String} urlPrefix */ function urlPrefix(this: MixtBuildURLMixin, path?: string | null, parentURL?: string): string { - let { host, namespace } = this; + const { namespace } = this; + let { host } = this; if (!host || host === '/') { host = ''; @@ -631,7 +629,7 @@ function urlPrefix(this: MixtBuildURLMixin, path?: string | null, parentURL?: st } // No path provided - let url: string[] = []; + const url: string[] = []; if (host) { url.push(host); } @@ -654,12 +652,11 @@ function urlPrefix(this: MixtBuildURLMixin, path?: string | null, parentURL?: st ```app/adapters/application.js import RESTAdapter from '@ember-data/adapter/rest'; - import { decamelize, pluralize } from '/utils/string-utils'; + import { undesrcore, pluralize } from '/utils/string-utils'; export default class ApplicationAdapter extends RESTAdapter { pathForType(modelName) { - var decamelized = decamelize(modelName); - return pluralize(decamelized); + return pluralize(underscore(modelName)); } } ``` @@ -670,7 +667,7 @@ function urlPrefix(this: MixtBuildURLMixin, path?: string | null, parentURL?: st @return {String} path **/ function pathForType(this: MixtBuildURLMixin, modelName: string): string { - let camelized = camelize(modelName); + const camelized = camelize(modelName); return pluralize(camelized); } @@ -693,4 +690,4 @@ const mixinProps: BuildURLMixin = { pathForType, }; -export default Mixin.create(mixinProps); +export const BuildURLMixin = Mixin.create(mixinProps); diff --git a/packages/adapter/src/-private/utils/continue-on-reject.ts b/packages/adapter/src/-private/utils/continue-on-reject.ts index beec65afe6a..4c1ec8cfc6b 100644 --- a/packages/adapter/src/-private/utils/continue-on-reject.ts +++ b/packages/adapter/src/-private/utils/continue-on-reject.ts @@ -1,3 +1,9 @@ -export default function continueOnReject(promise: Promise): Promise { - return Promise.resolve(promise).catch((e) => e); +/** + * A utility function that returns a promise that resolves + * even when the source promise rejects. + * + * @internal + */ +export function continueOnReject(promise: Promise): Promise { + return Promise.resolve(promise).catch((e) => e as T); } diff --git a/packages/adapter/src/-private/utils/determine-body-promise.ts b/packages/adapter/src/-private/utils/determine-body-promise.ts index 5e3baf9f6cc..b361c8db91a 100644 --- a/packages/adapter/src/-private/utils/determine-body-promise.ts +++ b/packages/adapter/src/-private/utils/determine-body-promise.ts @@ -1,12 +1,11 @@ import { warn } from '@ember/debug'; -import { DEBUG } from '@ember-data/env'; -import type { Dict } from '@ember-data/types/q/utils'; +import { DEBUG } from '@warp-drive/build-config/env'; import type { RequestData } from '../../rest'; -import continueOnReject from './continue-on-reject'; +import { continueOnReject } from './continue-on-reject'; -type Payload = Error | Dict | unknown[] | string | undefined; +type Payload = Error | Record | unknown[] | string | undefined; interface CustomSyntaxError extends SyntaxError { payload: Payload; @@ -25,13 +24,13 @@ function _determineContent(response: Response, requestData: JQueryAjaxSettings, return payload; } - let status = response.status; - let payloadIsEmpty = payload === '' || payload === null; - let statusIndicatesEmptyResponse = status === 204 || status === 205 || requestData.method === 'HEAD'; + const status = response.status; + const payloadIsEmpty = payload === '' || payload === null; + const statusIndicatesEmptyResponse = status === 204 || status === 205 || requestData.method === 'HEAD'; if (DEBUG) { if (payloadIsEmpty && !statusIndicatesEmptyResponse) { - let message = `The server returned an empty string for ${requestData.method} ${requestData.url}, which cannot be parsed into a valid JSON. Return either null or {}.`; + const message = `The server returned an empty string for ${requestData.method} ${requestData.url}, which cannot be parsed into a valid JSON. Return either null or {}.`; if (payload === '') { warn(message, { id: 'ds.adapter.returned-empty-string-as-JSON', @@ -45,7 +44,7 @@ function _determineContent(response: Response, requestData: JQueryAjaxSettings, } try { - ret = JSON.parse(payload as string); + ret = JSON.parse(payload as string) as Payload; } catch (e) { if (!(e instanceof SyntaxError)) { return e as Error; diff --git a/packages/adapter/src/-private/utils/fetch.ts b/packages/adapter/src/-private/utils/fetch.ts index 112db5dd98f..dc279595180 100644 --- a/packages/adapter/src/-private/utils/fetch.ts +++ b/packages/adapter/src/-private/utils/fetch.ts @@ -1,12 +1,12 @@ -import { assert } from '@ember/debug'; +import { assert } from '@warp-drive/build-config/macros'; -type FetchFunction = (input: RequestInfo, init?: RequestInit | undefined) => Promise; +type FetchFunction = (input: RequestInfo, init?: RequestInit) => Promise; let _fetch: (() => FetchFunction) | null = null; type MockRequest = { protocol?: string; get(key: string): string | undefined }; let REQUEST: MockRequest = null as unknown as MockRequest; -export default function getFetchFunction(): FetchFunction { +export function getFetchFunction(): FetchFunction { // return cached fetch function if (_fetch !== null) { return _fetch(); @@ -26,7 +26,6 @@ export default function getFetchFunction(): FetchFunction { const httpRegex = /^https?:\/\//; const protocolRelativeRegex = /^\/\//; - // eslint-disable-next-line no-inner-declarations function parseRequest(request: MockRequest) { if (request === null) { throw new Error( @@ -38,30 +37,32 @@ export default function getFetchFunction(): FetchFunction { return [request.get('host'), protocol]; } - // eslint-disable-next-line no-inner-declarations function buildAbsoluteUrl(url: string) { if (protocolRelativeRegex.test(url)) { - let [host] = parseRequest(REQUEST); + const [host] = parseRequest(REQUEST); url = host + url; } else if (!httpRegex.test(url)) { - let [host, protocol] = parseRequest(REQUEST); + const [host, protocol] = parseRequest(REQUEST); url = protocol + '//' + host + url; } return url; } - // eslint-disable-next-line no-inner-declarations - function patchedFetch(input, options) { - if (input && input.href) { - input.url = buildAbsoluteUrl(input.href); + function patchedFetch(input: string | { href: string } | RequestInfo, options?: RequestInit) { + if (input && typeof input === 'object' && 'href' in input) { + const url = buildAbsoluteUrl(input.href); + const info = Object.assign({}, input, { url }) as unknown as RequestInfo; + return nodeFetch(info, options); } else if (typeof input === 'string') { - input = buildAbsoluteUrl(input); + const url = buildAbsoluteUrl(input); + return nodeFetch(url, options); } + return nodeFetch(input, options); } _fetch = () => patchedFetch; - } catch (e) { + } catch { throw new Error(`Unable to create a compatible 'fetch' for FastBoot with node-fetch`); } } diff --git a/packages/adapter/src/-private/utils/parse-response-headers.ts b/packages/adapter/src/-private/utils/parse-response-headers.ts index f98d84edd93..09fd71011dc 100644 --- a/packages/adapter/src/-private/utils/parse-response-headers.ts +++ b/packages/adapter/src/-private/utils/parse-response-headers.ts @@ -1,9 +1,7 @@ -import type { Dict } from '@ember-data/types/q/utils'; - const newline = /\r?\n/; -export default function parseResponseHeaders(headersString: string): Dict { - const headers = Object.create(null) as Dict; +export function parseResponseHeaders(headersString: string): Record { + const headers = Object.create(null) as Record; if (!headersString) { return headers; @@ -12,7 +10,7 @@ export default function parseResponseHeaders(headersString: string): Dict(obj: unknown): obj is T { return Object.prototype.toString.call(obj) === '[object Object]'; } -/* - * Helper function that turns the data/body of a request into a query param string. - * This is directly copied from jQuery.param. - */ -export function serializeQueryParams(queryParamsObject: object | string): string { - let s: any[] = []; - function buildParams(prefix: string, obj: any) { - let i, len, key; - - if (prefix) { - if (Array.isArray(obj)) { - for (i = 0, len = obj.length; i < len; i++) { - if (RBRACKET.test(prefix)) { - add(s, prefix, obj[i]); - } else { - buildParams(prefix + '[' + (typeof obj[i] === 'object' && obj[i] !== null ? i : '') + ']', obj[i]); - } - } - } else if (isPlainObject(obj)) { - for (key in obj) { - buildParams(prefix + '[' + key + ']', obj[key]); - } - } else { - add(s, prefix, obj); - } - } else if (Array.isArray(obj)) { +function isPrimitiveArray(obj: unknown): obj is Array { + return Array.isArray(obj); +} + +function isParamsArray(obj: unknown): obj is ParamObject[] { + return Array.isArray(obj); +} + +function buildParams( + prefix: string, + obj: ParamObject | ParamObject[] | Array | T | string, + s: string[] +) { + let i: number, len: number, key: keyof T & string; + + if (prefix) { + if (isPrimitiveArray(obj)) { for (i = 0, len = obj.length; i < len; i++) { - add(s, obj[i].name, obj[i].value); + if (RBRACKET.test(prefix)) { + add(s, prefix, obj[i] as string); + } else { + buildParams(prefix + '[' + (typeof obj[i] === 'object' && obj[i] !== null ? i : '') + ']', obj[i], s); + } } - } else { + } else if (isPlainObject(obj)) { for (key in obj) { - buildParams(key, obj[key]); + buildParams(prefix + '[' + key + ']', obj[key], s); } + } else { + assert( + `query params cannot be a { name, value } pair if prefix is present`, + obj === null || typeof obj !== 'object' + ); + add(s, prefix, obj); + } + } else if (isParamsArray(obj)) { + for (i = 0, len = obj.length; i < len; i++) { + add(s, obj[i].name, obj[i].value); + } + } else { + assert(`query params cannot be a string if no prefix is present`, typeof obj !== 'string'); + assert(`query params should not be an array if no prefix is present`, !Array.isArray(obj)); + assert(`query params should not be a { name, value } pair if no prefix is present`, isPlainObject(obj)); + for (key in obj) { + buildParams(key, obj[key], s); } - return s; } + return s; +} - return buildParams('', queryParamsObject).join('&'); +/* + * Helper function that turns the data/body of a request into a query param string. + * This is directly copied from jQuery.param. + */ +export function serializeQueryParams(queryParamsObject: PlainObject | string): string { + return buildParams('', queryParamsObject, []).join('&'); } /* * Part of the `serializeQueryParams` helper function. */ -function add(s: Array, k: string, v?: string | (() => string)) { +function add(s: string[], k: string, v?: string | (() => string)) { // Strip out keys with undefined value and replace null values with // empty strings (mimics jQuery.ajax) if (v === undefined) { diff --git a/packages/adapter/src/error.js b/packages/adapter/src/error.js index e75193335ec..d21456c1334 100644 --- a/packages/adapter/src/error.js +++ b/packages/adapter/src/error.js @@ -1,9 +1,11 @@ /** @module @ember-data/adapter/error */ -import { assert, deprecate } from '@ember/debug'; +import { deprecate } from '@ember/debug'; -import { DEPRECATE_HELPERS } from '@ember-data/deprecations'; +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'; /** ## Overview @@ -52,7 +54,6 @@ import { DEPRECATE_HELPERS } from '@ember-data/deprecations'; `under-maintenance` route: ```app/routes/application.js - import Route from '@ember/routing/route'; import MaintenanceError from '../adapters/maintenance-error'; export default class ApplicationRoute extends Route { @@ -73,9 +74,9 @@ import { DEPRECATE_HELPERS } from '@ember-data/deprecations'; @class AdapterError @public */ -function AdapterError(errors, message = 'Adapter operation failed') { +function _AdapterError(errors, message = 'Adapter operation failed') { this.isAdapterError = true; - let error = Error.call(this, message); + const error = Error.call(this, message); if (error) { this.stack = error.stack; @@ -95,6 +96,12 @@ function AdapterError(errors, message = 'Adapter operation failed') { ]; } +_AdapterError.prototype = Object.create(Error.prototype); +_AdapterError.prototype.code = 'AdapterError'; +_AdapterError.extend = extendFn(_AdapterError); + +const AdapterError = getOrSetGlobal('AdapterError', _AdapterError); + export default AdapterError; function extendFn(ErrorClass) { @@ -104,7 +111,7 @@ function extendFn(ErrorClass) { } function extend(ParentErrorClass, defaultMessage) { - let ErrorClass = function (errors, message) { + const ErrorClass = function (errors, message) { assert('`AdapterError` expects json-api formatted errors array.', Array.isArray(errors || [])); ParentErrorClass.call(this, errors, message || defaultMessage); }; @@ -114,10 +121,6 @@ function extend(ParentErrorClass, defaultMessage) { return ErrorClass; } -AdapterError.prototype = Object.create(Error.prototype); -AdapterError.prototype.code = 'AdapterError'; -AdapterError.extend = extendFn(AdapterError); - /** A `InvalidError` is used by an adapter to signal the external API was unable to process a request because the content was not @@ -179,7 +182,10 @@ AdapterError.extend = extendFn(AdapterError); @extends AdapterError */ // TODO @deprecate extractError documentation -export const InvalidError = extend(AdapterError, 'The adapter rejected the commit because it was invalid'); +export const InvalidError = getOrSetGlobal( + 'InvalidError', + extend(AdapterError, 'The adapter rejected the commit because it was invalid') +); InvalidError.prototype.code = 'InvalidError'; /** @@ -191,9 +197,7 @@ InvalidError.prototype.code = 'InvalidError'; connection if an adapter operation has timed out: ```app/routes/application.js - import Route from '@ember/routing/route'; import { TimeoutError } from '@ember-data/adapter/error'; - import { action } from '@ember/object'; export default class ApplicationRoute extends Route { @action @@ -213,7 +217,7 @@ InvalidError.prototype.code = 'InvalidError'; @public @extends AdapterError */ -export const TimeoutError = extend(AdapterError, 'The adapter operation timed out'); +export const TimeoutError = getOrSetGlobal('TimeoutError', extend(AdapterError, 'The adapter operation timed out')); TimeoutError.prototype.code = 'TimeoutError'; /** @@ -226,7 +230,7 @@ TimeoutError.prototype.code = 'TimeoutError'; @public @extends AdapterError */ -export const AbortError = extend(AdapterError, 'The adapter operation was aborted'); +export const AbortError = getOrSetGlobal('AbortError', extend(AdapterError, 'The adapter operation was aborted')); AbortError.prototype.code = 'AbortError'; /** @@ -239,9 +243,7 @@ AbortError.prototype.code = 'AbortError'; request is unauthorized: ```app/routes/application.js - import Route from '@ember/routing/route'; import { UnauthorizedError } from '@ember-data/adapter/error'; - import { action } from '@ember/object'; export default class ApplicationRoute extends Route { @action @@ -261,7 +263,10 @@ AbortError.prototype.code = 'AbortError'; @public @extends AdapterError */ -export const UnauthorizedError = extend(AdapterError, 'The adapter operation is unauthorized'); +export const UnauthorizedError = getOrSetGlobal( + 'UnauthorizedError', + extend(AdapterError, 'The adapter operation is unauthorized') +); UnauthorizedError.prototype.code = 'UnauthorizedError'; /** @@ -275,7 +280,10 @@ UnauthorizedError.prototype.code = 'UnauthorizedError'; @public @extends AdapterError */ -export const ForbiddenError = extend(AdapterError, 'The adapter operation is forbidden'); +export const ForbiddenError = getOrSetGlobal( + 'ForbiddenError', + extend(AdapterError, 'The adapter operation is forbidden') +); ForbiddenError.prototype.code = 'ForbiddenError'; /** @@ -287,10 +295,7 @@ ForbiddenError.prototype.code = 'ForbiddenError'; for a specific model that does not exist. For example: ```app/routes/post.js - import Route from '@ember/routing/route'; import { NotFoundError } from '@ember-data/adapter/error'; - import { inject as service } from '@ember/service'; - import { action } from '@ember/object'; export default class PostRoute extends Route { @service store; @@ -314,7 +319,10 @@ ForbiddenError.prototype.code = 'ForbiddenError'; @public @extends AdapterError */ -export const NotFoundError = extend(AdapterError, 'The adapter could not find the resource'); +export const NotFoundError = getOrSetGlobal( + 'NotFoundError', + extend(AdapterError, 'The adapter could not find the resource') +); NotFoundError.prototype.code = 'NotFoundError'; /** @@ -328,7 +336,10 @@ NotFoundError.prototype.code = 'NotFoundError'; @public @extends AdapterError */ -export const ConflictError = extend(AdapterError, 'The adapter operation failed due to a conflict'); +export const ConflictError = getOrSetGlobal( + 'ConflictError', + extend(AdapterError, 'The adapter operation failed due to a conflict') +); ConflictError.prototype.code = 'ConflictError'; /** @@ -340,7 +351,10 @@ ConflictError.prototype.code = 'ConflictError'; @public @extends AdapterError */ -export const ServerError = extend(AdapterError, 'The adapter operation failed due to a server error'); +export const ServerError = getOrSetGlobal( + 'ServerError', + extend(AdapterError, 'The adapter operation failed due to a server error') +); ServerError.prototype.code = 'ServerError'; function makeArray(value) { @@ -400,11 +414,11 @@ export function errorsHashToArray(errors) { until: '5.0', since: { available: '4.7', enabled: '4.7' }, }); - let out = []; + const out = []; if (errors) { Object.keys(errors).forEach((key) => { - let messages = makeArray(errors[key]); + const messages = makeArray(errors[key]); for (let i = 0; i < messages.length; i++) { let title = 'Invalid Attribute'; let pointer = `/data/attributes/${key}`; @@ -475,7 +489,7 @@ export function errorsArrayToHash(errors) { until: '5.0', since: { available: '4.7', enabled: '4.7' }, }); - let out = {}; + const out = {}; if (errors) { errors.forEach((error) => { diff --git a/packages/adapter/src/index.ts b/packages/adapter/src/index.ts index 8d59db0644a..11375333241 100644 --- a/packages/adapter/src/index.ts +++ b/packages/adapter/src/index.ts @@ -67,17 +67,19 @@ your Adapter does not need the method. ```ts - import EmberObject from '@ember/object'; - async function fetchData(url, options = {}) { let response = await fetch(url, options); return response.toJSON(); } - export default class ApplicationAdapter extends EmberObject { + export default class ApplicationAdapter { findRecord(_, { modelName }, id) { return fetchData(`./${modelName}s/${id}`); } + + static create() { + return new this(); + } } ``` @@ -147,7 +149,7 @@ Note: If you are using Ember and would like to make use of `service` injections ```js import Store from '@ember-data/store'; import Adapter from '@ember-data/adapter/json-api'; - import { getOwner, setOwner } from '@ember/application'; + import { getOwner, setOwner } from '@ember/owner'; class extends Store { #adapter = null; @@ -188,12 +190,12 @@ By default when using with Ember you only need to implement this hook if you wan import EmberObject from '@ember/object'; import { inject as service } from '@ember/service'; -import { DEBUG } from '@ember-data/env'; +import type { AdapterPayload, MinimumAdapterInterface, SerializerOptions } from '@ember-data/legacy-compat'; import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import type { AdapterPayload, MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; -import type { Dict } from '@ember-data/types/q/utils'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; /** An adapter is an object that receives requests from a store and @@ -294,7 +296,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { + findRecord(store: Store, type: ModelSchema, id: string, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a findRecord override'); } @@ -326,15 +328,15 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @method findAll @param {Store} store @param {Model} type - @param {undefined} neverSet a value is never provided to this argument + @param {null} neverSet a value is never provided to this argument @param {SnapshotRecordArray} snapshotRecordArray @return {Promise} promise @public */ findAll( store: Store, - type: ShimModelClass, - neverSet, + type: ModelSchema, + neverSet: null, snapshotRecordArray: SnapshotRecordArray // @ts-expect-error ): Promise { @@ -376,7 +378,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - query(store: Store, type: ShimModelClass, query): Promise { + query(store: Store, type: ModelSchema, query): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a query override'); } @@ -421,7 +423,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - queryRecord(store: Store, type: ShimModelClass, query, adapterOptions): Promise { + queryRecord(store: Store, type: ModelSchema, query, adapterOptions): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a queryRecord override'); } @@ -485,8 +487,13 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Object} serialized snapshot @public */ - serialize(snapshot, options): Dict { - return snapshot.serialize(options); + serialize(snapshot: Snapshot, options: SerializerOptions): Record { + const serialized = snapshot.serialize(options); + assert( + `Your adapter's serialize method must return an object, but it returned ${typeof serialized}`, + serialized && typeof serialized === 'object' + ); + return serialized as Record; } /** @@ -531,7 +538,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + createRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a createRecord override'); } @@ -588,7 +595,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a updateRecord override'); } @@ -637,7 +644,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @public */ // @ts-expect-error - deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + deleteRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a deleteRecord override'); } @@ -654,7 +661,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @type {boolean} */ get coalesceFindRequests() { - let coalesceFindRequests = this._coalesceFindRequests; + const coalesceFindRequests = this._coalesceFindRequests; if (typeof coalesceFindRequests === 'boolean') { return coalesceFindRequests; } @@ -863,7 +870,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Boolean} @public */ - shouldBackgroundReloadRecord(store: Store, Snapshot): boolean { + shouldBackgroundReloadRecord(store: Store, snapshot: Snapshot): boolean { return true; } diff --git a/packages/adapter/src/json-api.ts b/packages/adapter/src/json-api.ts index 90d344dc2f6..1048c8c3eac 100644 --- a/packages/adapter/src/json-api.ts +++ b/packages/adapter/src/json-api.ts @@ -1,246 +1,244 @@ /** @module @ember-data/adapter/json-api */ -import { assert } from '@ember/debug'; -import { dasherize } from '@ember/string'; - -import { pluralize } from 'ember-inflector'; - -import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import type { AdapterPayload } from '@ember-data/legacy-compat'; +import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; +import { dasherize, pluralize } from '@ember-data/request-utils/string'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; +import type { ModelSchema } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { HTTPMethod } from '@warp-drive/core-types/request'; import { serializeIntoHash } from './-private'; -import type { FetchRequestInit, JQueryRequestInit } from './rest'; +import type { FetchRequestInit, JQueryRequestInit, QueryState } from './rest'; import RESTAdapter from './rest'; /** - ## Overview - -
-

- ⚠️ This is LEGACY documentation for a feature that is no longer encouraged to be used. - If starting a new app or thinking of implementing a new adapter, consider writing a - Handler instead to be used with the RequestManager -

-
- - The `JSONAPIAdapter` is an adapter whichtransforms the store's - requests into HTTP requests that follow the [JSON API format](http://jsonapi.org/format/). - - ## JSON API Conventions - - The JSONAPIAdapter uses JSON API conventions for building the URL - for a record and selecting the HTTP verb to use with a request. The - actions you can take on a record map onto the following URLs in the - JSON API adapter: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Action - - HTTP Verb - - URL -
- `store.findRecord('post', 123)` - - GET - - /posts/123 -
- `store.findAll('post')` - - GET - - /posts -
- Update `postRecord.save()` - - PATCH - - /posts/123 -
- Create `store.createRecord('post').save()` - - POST - - /posts -
- Delete `postRecord.destroyRecord()` - - DELETE - - /posts/123 -
- - ## Success and failure - - The JSONAPIAdapter will consider a success any response with a - status code of the 2xx family ("Success"), as well as 304 ("Not - Modified"). Any other status code will be considered a failure. - - On success, the request promise will be resolved with the full - response payload. - - Failed responses with status code 422 ("Unprocessable Entity") will - be considered "invalid". The response will be discarded, except for - the `errors` key. The request promise will be rejected with a - `InvalidError`. This error object will encapsulate the saved - `errors` value. - - Any other status codes will be treated as an adapter error. The - request promise will be rejected, similarly to the invalid case, - but with an instance of `AdapterError` instead. - - ### Endpoint path customization - - Endpoint paths can be prefixed with a `namespace` by setting the - namespace property on the adapter: - - ```app/adapters/application.js - import JSONAPIAdapter from '@ember-data/adapter/json-api'; - - export default class ApplicationAdapter extends JSONAPIAdapter { - namespace = 'api/1'; - } - ``` - Requests for the `person` model would now target `/api/1/people/1`. + ## Overview + +
+

+ ⚠️ This is LEGACY documentation for a feature that is no longer encouraged to be used. + If starting a new app or thinking of implementing a new adapter, consider writing a + Handler instead to be used with the RequestManager +

+
+ + The `JSONAPIAdapter` is an adapter whichtransforms the store's + requests into HTTP requests that follow the [JSON API format](http://jsonapi.org/format/). + + ## JSON API Conventions + + The JSONAPIAdapter uses JSON API conventions for building the URL + for a record and selecting the HTTP verb to use with a request. The + actions you can take on a record map onto the following URLs in the + JSON API adapter: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Action + + HTTP Verb + + URL +
+ `store.findRecord('post', 123)` + + GET + + /posts/123 +
+ `store.findAll('post')` + + GET + + /posts +
+ Update `postRecord.save()` + + PATCH + + /posts/123 +
+ Create `store.createRecord('post').save()` + + POST + + /posts +
+ Delete `postRecord.destroyRecord()` + + DELETE + + /posts/123 +
+ + ## Success and failure + + The JSONAPIAdapter will consider a success any response with a + status code of the 2xx family ("Success"), as well as 304 ("Not + Modified"). Any other status code will be considered a failure. + + On success, the request promise will be resolved with the full + response payload. + + Failed responses with status code 422 ("Unprocessable Entity") will + be considered "invalid". The response will be discarded, except for + the `errors` key. The request promise will be rejected with a + `InvalidError`. This error object will encapsulate the saved + `errors` value. + + Any other status codes will be treated as an adapter error. The + request promise will be rejected, similarly to the invalid case, + but with an instance of `AdapterError` instead. + + ### Endpoint path customization + + Endpoint paths can be prefixed with a `namespace` by setting the + namespace property on the adapter: + + ```app/adapters/application.js + import JSONAPIAdapter from '@ember-data/adapter/json-api'; + + export default class ApplicationAdapter extends JSONAPIAdapter { + namespace = 'api/1'; + } + ``` + Requests for the `person` model would now target `/api/1/people/1`. - ### Host customization + ### Host customization - An adapter can target other hosts by setting the `host` property. + An adapter can target other hosts by setting the `host` property. - ```app/adapters/application.js - import JSONAPIAdapter from '@ember-data/adapter/json-api'; + ```app/adapters/application.js + import JSONAPIAdapter from '@ember-data/adapter/json-api'; - export default class ApplicationAdapter extends JSONAPIAdapter { - host = 'https://api.example.com'; - } - ``` - - Requests for the `person` model would now target - `https://api.example.com/people/1`. - - @since 1.13.0 - @class JSONAPIAdapter - @main @ember-data/adapter/json-api - @public - @constructor - @extends RESTAdapter -*/ + export default class ApplicationAdapter extends JSONAPIAdapter { + host = 'https://api.example.com'; + } + ``` + + Requests for the `person` model would now target + `https://api.example.com/people/1`. + + @since 1.13.0 + @class JSONAPIAdapter + @main @ember-data/adapter/json-api + @public + @constructor + @extends RESTAdapter + */ class JSONAPIAdapter extends RESTAdapter { _defaultContentType = 'application/vnd.api+json'; /** - @method ajaxOptions - @private - @param {String} url - @param {String} type The request type GET, POST, PUT, DELETE etc. - @param {Object} options - @return {Object} - */ + @method ajaxOptions + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} options + @return {Object} + */ ajaxOptions( url: string, - type: string, + type: HTTPMethod, options: JQueryAjaxSettings | RequestInit = {} ): JQueryRequestInit | FetchRequestInit { - let hash = super.ajaxOptions(url, type, options); - - hash.headers['Accept'] = hash.headers['Accept'] || 'application/vnd.api+json'; + const hash = super.ajaxOptions(url, type, options) as FetchRequestInit; + const headers: HeadersInit = (hash.headers = hash.headers || {}); + headers['Accept'] = (headers['Accept'] as string) || 'application/vnd.api+json'; return hash; } /** - By default the JSONAPIAdapter will send each find request coming from a `store.find` - or from accessing a relationship separately to the server. If your server supports passing - ids as a query string, you can set coalesceFindRequests to true to coalesce all find requests - within a single runloop. - - For example, if you have an initial payload of: - - ```javascript - { - data: { - id: 1, - type: 'post', - relationship: { - comments: { - data: [ - { id: 1, type: 'comment' }, - { id: 2, type: 'comment' } - ] + By default the JSONAPIAdapter will send each find request coming from a `store.find` + or from accessing a relationship separately to the server. If your server supports passing + ids as a query string, you can set coalesceFindRequests to true to coalesce all find requests + within a single runloop. + + For example, if you have an initial payload of: + + ```javascript + { + data: { + id: 1, + type: 'post', + relationship: { + comments: { + data: [ + { id: 1, type: 'comment' }, + { id: 2, type: 'comment' } + ] + } } } } - } - ``` + ``` - By default calling `post.comments` will trigger the following requests(assuming the - comments haven't been loaded before): + By default calling `post.comments` will trigger the following requests(assuming the + comments haven't been loaded before): - ``` - GET /comments/1 - GET /comments/2 - ``` + ``` + GET /comments/1 + GET /comments/2 + ``` - If you set coalesceFindRequests to `true` it will instead trigger the following request: + If you set coalesceFindRequests to `true` it will instead trigger the following request: - ``` - GET /comments?filter[id]=1,2 - ``` + ``` + GET /comments?filter[id]=1,2 + ``` - Setting coalesceFindRequests to `true` also works for `store.find` requests and `belongsTo` - relationships accessed within the same runloop. If you set `coalesceFindRequests: true` + Setting coalesceFindRequests to `true` also works for `store.find` requests and `belongsTo` + relationships accessed within the same runloop. If you set `coalesceFindRequests: true` - ```javascript - store.findRecord('comment', 1); - store.findRecord('comment', 2); - ``` + ```javascript + store.findRecord('comment', 1); + store.findRecord('comment', 2); + ``` - will also send a request to: `GET /comments?filter[id]=1,2` + will also send a request to: `GET /comments?filter[id]=1,2` - Note: Requests coalescing rely on URL building strategy. So if you override `buildURL` in your app - `groupRecordsForFindMany` more likely should be overridden as well in order for coalescing to work. + Note: Requests coalescing rely on URL building strategy. So if you override `buildURL` in your app + `groupRecordsForFindMany` more likely should be overridden as well in order for coalescing to work. - @property coalesceFindRequests - @public - @type {boolean} - */ + @property coalesceFindRequests + @public + @type {boolean} + */ get coalesceFindRequests() { - let coalesceFindRequests = this._coalesceFindRequests; + const coalesceFindRequests = this._coalesceFindRequests; if (typeof coalesceFindRequests === 'boolean') { return coalesceFindRequests; } @@ -251,26 +249,51 @@ class JSONAPIAdapter extends RESTAdapter { this._coalesceFindRequests = value; } - findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { - let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); + findMany(store: Store, type: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise { + const url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } }); } - pathForType(modelName): string { - let dasherized = dasherize(modelName); + pathForType(modelName: string): string { + const dasherized = dasherize(modelName); return pluralize(dasherized); } - updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, schema, snapshot); const type = snapshot.modelName; const id = snapshot.id; assert(`Attempted to update the ${type} record, but the record has no id`, typeof id === 'string' && id.length > 0); - let url = this.buildURL(type, id, snapshot, 'updateRecord'); + const url = this.buildURL(type, id, snapshot, 'updateRecord'); return this.ajax(url, 'PATCH', { data: data }); } + + /** + Used by `findAll` and `findRecord` to build the query's `data` hash + supplied to the ajax method. + + @method buildQuery + @since 2.5.0 + @public + @param {Snapshot} snapshot + @return {Object} + */ + buildQuery(snapshot: Snapshot | SnapshotRecordArray): QueryState { + const query: QueryState = {}; + + if (snapshot) { + const { include } = snapshot; + const normalizedInclude = Array.isArray(include) ? include.join(',') : include; + + if (normalizedInclude) { + query.include = normalizedInclude; + } + } + + return query; + } } export default JSONAPIAdapter; diff --git a/packages/adapter/src/rest.ts b/packages/adapter/src/rest.ts index 4453b384c40..827db259e5d 100644 --- a/packages/adapter/src/rest.ts +++ b/packages/adapter/src/rest.ts @@ -1,19 +1,23 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ /** @module @ember-data/adapter/rest */ import { getOwner } from '@ember/application'; -import { assert, warn } from '@ember/debug'; +import { warn } from '@ember/debug'; import { computed } from '@ember/object'; -import { join } from '@ember/runloop'; -import { DEBUG } from '@ember-data/env'; +import type { AdapterPayload } from '@ember-data/legacy-compat'; import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; -import type { Dict } from '@ember-data/types/q/utils'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { HTTPMethod } from '@warp-drive/core-types/request'; import { determineBodyPromise, fetch, parseResponseHeaders, serializeIntoHash, serializeQueryParams } from './-private'; +import type { MixtBuildURLMixin } from './-private/build-url-mixin'; import type { FastBoot } from './-private/fastboot-interface'; import AdapterError, { AbortError, @@ -27,41 +31,36 @@ import AdapterError, { } from './error'; import Adapter, { BuildURLMixin } from './index'; -type Payload = Error | Dict | unknown[] | string | undefined; +type Payload = Error | Record | unknown[] | string | undefined; -type QueryState = { +export type QueryState = { include?: unknown; since?: unknown; }; export interface FetchRequestInit extends RequestInit { url: string; - method: string; - type: string; - contentType?: any; - body?: any; - data?: any; - cache?: any; - headers?: any; + method: HTTPMethod; + type: HTTPMethod; } export interface JQueryRequestInit extends JQueryAjaxSettings { url: string; - method: string; - type: string; + method: HTTPMethod; + type: HTTPMethod; } export type RequestData = { url: string; - method: string; - [key: string]: any; + method: HTTPMethod; + [key: string]: unknown; }; type ResponseData = { status: number; textStatus: string; - headers: Dict; - errorThrown?: any; + headers: Record; + errorThrown?: Error | string; }; declare const jQuery: JQueryStatic | undefined; @@ -115,7 +114,7 @@ declare const jQuery: JQueryStatic | undefined; { "posts": { "id": 1, - "title": "I'm Running to Reform the W3C's Tag", + "title": "I'm Running to Reform the W3C", "author": "Yehuda Katz" } } @@ -129,7 +128,7 @@ declare const jQuery: JQueryStatic | undefined; "posts": [ { "id": 1, - "title": "I'm Running to Reform the W3C's Tag", + "title": "I'm Running to Reform the W3C", "author": "Yehuda Katz" }, { @@ -187,7 +186,7 @@ declare const jQuery: JQueryStatic | undefined; { "posts": { "id": 5, - "title": "I'm Running to Reform the W3C's Tag", + "title": "I'm Running to Reform the W3C", "author": "Yehuda Katz", "comments": [1, 2] }, @@ -213,7 +212,7 @@ declare const jQuery: JQueryStatic | undefined; { "posts": { "id": 5, - "title": "I'm Running to Reform the W3C's Tag", + "title": "I'm Running to Reform the W3C", "author": "Yehuda Katz", "links": { "comments": "/posts/5/comments" @@ -272,15 +271,14 @@ declare const jQuery: JQueryStatic | undefined; Some APIs require HTTP headers, e.g. to provide an API key. Arbitrary headers can be set as key/value pairs on the `RESTAdapter`'s `headers` - object and Ember Data will send them along with each ajax request. + object and EmberData will send them along with each ajax request. ```app/adapters/application.js import RESTAdapter from '@ember-data/adapter/rest'; - import { computed } from '@ember/object'; export default class ApplicationAdapter extends RESTAdapter { - headers: computed(function() { + get headers() { return { 'API_KEY': 'secret key', 'ANOTHER_HEADER': 'Some header value' @@ -289,45 +287,6 @@ declare const jQuery: JQueryStatic | undefined; } ``` - `headers` can also be used as a computed property to support dynamic - headers. In the example below, the `session` object has been - injected into an adapter by Ember's container. - - ```app/adapters/application.js - import RESTAdapter from '@ember-data/adapter/rest'; - import { computed } from '@ember/object'; - - export default class ApplicationAdapter extends RESTAdapter { - headers: computed('session.authToken', function() { - return { - 'API_KEY': this.session.authToken, - 'ANOTHER_HEADER': 'Some header value' - }; - }) - } - ``` - - In some cases, your dynamic headers may require data from some - object outside of Ember's observer system (for example - `document.cookie`). You can use the - [volatile](/api/classes/Ember.ComputedProperty.html?anchor=volatile) - function to set the property into a non-cached mode causing the headers to - be recomputed with every request. - - ```app/adapters/application.js - import RESTAdapter from '@ember-data/adapter/rest'; - import { computed } from '@ember/object'; - - export default class ApplicationAdapter extends RESTAdapter { - headers: computed(function() { - return { - 'API_KEY': document.cookie.match(/apiKey\=([^;]*)/)['1'], - 'ANOTHER_HEADER': 'Some header value' - }; - }).volatile() - } - ``` - @class RESTAdapter @main @ember-data/adapter/rest @public @@ -337,6 +296,7 @@ declare const jQuery: JQueryStatic | undefined; */ class RESTAdapter extends Adapter.extend(BuildURLMixin) { declare _fastboot: FastBoot; + declare _coalesceFindRequests: boolean; declare host: string | null; declare namespace: string | null; @@ -356,11 +316,11 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { get fastboot() { // Avoid computed property override deprecation in fastboot as suggested by: // https://deprecations.emberjs.com/v3.x/#toc_computed-property-override - let fastboot = this._fastboot; + const fastboot = this._fastboot; if (fastboot) { return fastboot; } - return (this._fastboot = (getOwner(this) as any).lookup('service:fastboot')); + return (this._fastboot = getOwner(this)!.lookup('service:fastboot') as FastBoot); } set fastboot(value: FastBoot) { @@ -410,14 +370,14 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @return {Object} @public */ - sortQueryParams(obj): Dict { - let keys = Object.keys(obj); - let len = keys.length; + sortQueryParams(obj: Record): Record { + const keys = Object.keys(obj); + const len = keys.length; if (len < 2) { return obj; } - let newQueryParams = {}; - let sortedKeys = keys.sort(); + const newQueryParams: Record = {}; + const sortedKeys = keys.sort(); for (let i = 0; i < len; i++) { newQueryParams[sortedKeys[i]] = obj[sortedKeys[i]]; @@ -474,7 +434,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @type {boolean} */ get coalesceFindRequests() { - let coalesceFindRequests = this._coalesceFindRequests; + const coalesceFindRequests = this._coalesceFindRequests; if (typeof coalesceFindRequests === 'boolean') { return coalesceFindRequests; } @@ -531,15 +491,14 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { ```app/adapters/application.js import RESTAdapter from '@ember-data/adapter/rest'; - import { computed } from '@ember/object'; export default class ApplicationAdapter extends RESTAdapter { - headers: computed(function() { + get headers() { return { 'API_KEY': 'secret key', 'ANOTHER_HEADER': 'Some header value' }; - }) + } } ``` @@ -547,7 +506,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @public @type {Object} */ - declare headers: Dict | undefined; + declare headers: Record | undefined; /** Called by the store in order to fetch the JSON for a given @@ -567,9 +526,9 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { - let url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); - let query: QueryState = this.buildQuery(snapshot); + findRecord(store: Store, type: ModelSchema, id: string, snapshot: Snapshot): Promise { + const url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); + const query: QueryState = this.buildQuery(snapshot); return this.ajax(url, 'GET', { data: query }); } @@ -591,12 +550,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { */ findAll( store: Store, - type: ShimModelClass, - sinceToken, + type: ModelSchema, + sinceToken: null, snapshotRecordArray: SnapshotRecordArray ): Promise { - let query: QueryState = this.buildQuery(snapshotRecordArray); - let url = this.buildURL(type.modelName, null, snapshotRecordArray, 'findAll'); + const query: QueryState = this.buildQuery(snapshotRecordArray); + const url = this.buildURL(type.modelName, null, snapshotRecordArray, 'findAll'); if (sinceToken) { query.since = sinceToken; @@ -625,8 +584,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} adapterOptions @return {Promise} promise */ - query(store: Store, type: ShimModelClass, query): Promise { - let url = this.buildURL(type.modelName, null, null, 'query', query); + query(store: Store, type: ModelSchema, query: Record): Promise { + const url = this.buildURL(type.modelName, null, null, 'query', query); if (this.sortQueryParams) { query = this.sortQueryParams(query); @@ -657,11 +616,11 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { */ queryRecord( store: Store, - type: ShimModelClass, - query: Dict, - adapterOptions: Dict + type: ModelSchema, + query: Record, + adapterOptions: Record ): Promise { - let url = this.buildURL(type.modelName, null, null, 'queryRecord', query); + const url = this.buildURL(type.modelName, null, null, 'queryRecord', query); if (this.sortQueryParams) { query = this.sortQueryParams(query); @@ -704,8 +663,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Array} snapshots @return {Promise} promise */ - findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { - let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); + findMany(store: Store, type: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise { + const url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { ids: ids } }); } @@ -746,9 +705,14 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} relationship meta object describing the relationship @return {Promise} promise */ - findHasMany(store: Store, snapshot: Snapshot, url: string, relationship: Dict): Promise { - let id = snapshot.id; - let type = snapshot.modelName; + findHasMany( + store: Store, + snapshot: Snapshot, + url: string, + relationship: Record + ): Promise { + const id = snapshot.id; + const type = snapshot.modelName; assert( `Attempted to fetch the hasMany relationship for ${type}, but the record has no id`, @@ -797,8 +761,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @return {Promise} promise */ findBelongsTo(store: Store, snapshot: Snapshot, url: string, relationship): Promise { - let id = snapshot.id; - let type = snapshot.modelName; + const id = snapshot.id; + const type = snapshot.modelName; assert( `Attempted to fetch the belongsTo relationship for ${type}, but the record has no id`, @@ -825,8 +789,8 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { - let url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); + createRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise { + const url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); const data = serializeIntoHash(store, type, snapshot); @@ -850,12 +814,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, schema, snapshot, {}); const type = snapshot.modelName; const id = snapshot.id; assert(`Attempted to update the ${type} record, but the record has no id`, typeof id === 'string' && id.length > 0); - let url = this.buildURL(type, id, snapshot, 'updateRecord'); + const url = this.buildURL(type, id, snapshot, 'updateRecord'); return this.ajax(url, 'PUT', { data }); } @@ -872,7 +836,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - deleteRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + deleteRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise { const type = snapshot.modelName; const id = snapshot.id; assert(`Attempted to delete the ${type} record, but the record has no id`, typeof id === 'string' && id.length > 0); @@ -888,15 +852,15 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { typeof id === 'string' && id.length > 0 ); - let url = this.buildURL(type, id, snapshot); + const url = this.buildURL(type, id, snapshot); - let expandedURL = url.split('/'); + const expandedURL = url.split('/'); // Case when the url is of the format ...something/:id // We are decodeURIComponent-ing the lastSegment because if it represents // the id, it has been encodeURIComponent-ified within `buildURL`. If we // don't do this, then records with id having special characters are not // coalesced correctly (see GH #4190 for the reported bug) - let lastSegment: string = expandedURL[expandedURL.length - 1]; + const lastSegment: string = expandedURL[expandedURL.length - 1]; if (decodeURIComponent(lastSegment) === id) { expandedURL[expandedURL.length - 1] = ''; } else if (id && endsWith(lastSegment, '?id=' + id)) { @@ -934,44 +898,22 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { loaded separately by `findMany`. */ groupRecordsForFindMany(store: Store, snapshots: Snapshot[]): Snapshot[][] { - let groups = new Map(); - let adapter = this; - let maxURLLength = this.maxURLLength; + const groups: Map = new Map(); + const maxURLLength = this.maxURLLength; snapshots.forEach((snapshot) => { - let baseUrl = adapter._stripIDFromURL(store, snapshot); + const baseUrl = this._stripIDFromURL(store, snapshot); if (!groups.has(baseUrl)) { groups.set(baseUrl, []); } - groups.get(baseUrl).push(snapshot); + groups.get(baseUrl)!.push(snapshot); }); - function splitGroupToFitInUrl(group, maxURLLength, paramNameLength) { - let idsSize = 0; - let baseUrl = adapter._stripIDFromURL(store, group[0]); - let splitGroups: Snapshot[][] = [[]]; - - group.forEach((snapshot) => { - let additionalLength = encodeURIComponent(snapshot.id).length + paramNameLength; - if (baseUrl.length + idsSize + additionalLength >= maxURLLength) { - idsSize = 0; - splitGroups.push([]); - } - - idsSize += additionalLength; - - let lastGroupIndex = splitGroups.length - 1; - splitGroups[lastGroupIndex].push(snapshot); - }); - - return splitGroups; - } - - let groupsArray: Snapshot[][] = []; + const groupsArray: Snapshot[][] = []; groups.forEach((group, key) => { - let paramNameLength = '&ids%5B%5D='.length; - let splitGroups = splitGroupToFitInUrl(group, maxURLLength, paramNameLength); + const paramNameLength = '&ids%5B%5D='.length; + const splitGroups = splitGroupToFitInUrl(store, this, group, maxURLLength, paramNameLength); splitGroups.forEach((splitGroup) => groupsArray.push(splitGroup)); }); @@ -1011,18 +953,18 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { */ handleResponse( status: number, - headers: Dict, + headers: Record, payload: Payload, requestData: RequestData - ): Payload | AdapterError { + ): Payload | typeof AdapterError { if (this.isSuccess(status, headers, payload)) { return payload; } else if (this.isInvalid(status, headers, payload)) { return new InvalidError(typeof payload === 'object' && 'errors' in payload ? payload.errors : undefined); } - let errors = this.normalizeErrorResponse(status, headers, payload); - let detailedMessage = this.generatedDetailedMessage(status, headers, payload, requestData); + const errors = this.normalizeErrorResponse(status, headers, payload); + const detailedMessage = this.generatedDetailedMessage(status, headers, payload, requestData); switch (status) { case 401: @@ -1039,7 +981,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { } } - return new AdapterError(errors, detailedMessage); + return new AdapterError(errors, detailedMessage) as unknown as typeof AdapterError; } /** @@ -1054,7 +996,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Boolean} */ - isSuccess(status: number, _headers: Dict, _payload: Payload): boolean { + isSuccess(status: number, _headers: Record, _payload: Payload): boolean { return (status >= 200 && status < 300) || status === 304; } @@ -1070,7 +1012,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Boolean} */ - isInvalid(status: number, _headers: Dict, _payload: Payload): boolean { + isInvalid(status: number, _headers: Record, _payload: Payload): boolean { return status === 422; } @@ -1098,40 +1040,26 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} options @return {Promise} promise */ - async ajax(url: string, type: string, options: JQueryAjaxSettings | RequestInit = {}): Promise { - let adapter = this; - - let requestData: RequestData = { + async ajax(url: string, type: HTTPMethod, options: JQueryAjaxSettings | RequestInit = {}): Promise { + const requestData: RequestData = { url: url, method: type, }; if (this.useFetch) { - let hash: FetchRequestInit = adapter.ajaxOptions(url, type, options); - let response = await this._fetchRequest(hash); - let payload = await determineBodyPromise(response, requestData); + // @ts-expect-error poorly typed + const hash: FetchRequestInit = this.ajaxOptions(url, type, options); + const response = await this._fetchRequest(hash); + const payload = await determineBodyPromise(response, requestData); if (response.ok && !(payload instanceof Error)) { - return fetchSuccessHandler(adapter, payload, response, requestData); + return fetchSuccessHandler(this, payload, response, requestData); } else { - throw fetchErrorHandler(adapter, payload, response, null, requestData); + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw fetchErrorHandler(this, payload, response, null, requestData); } } else { - let hash: JQueryRequestInit = adapter.ajaxOptions(url, type, options); - - return new Promise(function (resolve, reject) { - hash.success = function (payload, textStatus, jqXHR) { - let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); - join(null, resolve, response); - }; - - hash.error = function (jqXHR, textStatus, errorThrown) { - let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); - join(null, reject, error); - }; - - adapter._ajax(hash); - }); + return execjQAjax(this, requestData, options as JQueryAjaxSettings); } } @@ -1141,19 +1069,19 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} options jQuery ajax options to be used for the ajax request */ _ajaxRequest(options: JQueryRequestInit): void { - // TODO add assertion that jquery is there rather then equality check - typeof jQuery !== 'undefined' && jQuery.ajax(options); + assert('You must install jQuery globally when `useFetch` is false', typeof jQuery !== 'undefined'); + void jQuery.ajax(options); } _fetchRequest(options: FetchRequestInit): Promise { - let fetchFunction = fetch(); + const fetchFunction = fetch(); return fetchFunction(options.url, options); } _ajax(options: FetchRequestInit | JQueryRequestInit): void { if (this.useFetch) { - this._fetchRequest(options as FetchRequestInit); + void this._fetchRequest(options as FetchRequestInit); } else { this._ajaxRequest(options as JQueryRequestInit); } @@ -1169,7 +1097,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { */ ajaxOptions( url: string, - method: string, + method: HTTPMethod, options: JQueryAjaxSettings | RequestInit ): JQueryRequestInit | FetchRequestInit { let reqOptions: JQueryRequestInit | FetchRequestInit = Object.assign( @@ -1182,26 +1110,32 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { ); if (this.headers !== undefined) { + // @ts-expect-error poorly typed reqOptions.headers = { ...this.headers, ...reqOptions.headers }; } else if (!options.headers) { reqOptions.headers = {}; } - let contentType = reqOptions.contentType || this._defaultContentType; + // @ts-expect-error poorly typed + const contentType = reqOptions.contentType || this._defaultContentType; if (this.useFetch) { + // @ts-expect-error poorly typed if (reqOptions.data && reqOptions.type !== 'GET' && reqOptions.headers) { if (!reqOptions.headers['Content-Type'] && !reqOptions.headers['content-type']) { reqOptions.headers['content-type'] = contentType; } } + // @ts-expect-error poorly typed reqOptions = fetchOptions(reqOptions, this); } else { // GET requests without a body should not have a content-type header // and may be unexpected by a server + // @ts-expect-error poorly typed if (reqOptions.data && reqOptions.type !== 'GET') { reqOptions = { ...reqOptions, contentType }; } + // @ts-expect-error poorly typed reqOptions = ajaxOptions(reqOptions, this); } @@ -1212,10 +1146,10 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { _ajaxURL(url: string): string { if (this.fastboot?.isFastBoot) { - let httpRegex = /^https?:\/\//; - let protocolRelativeRegex = /^\/\//; - let protocol = this.fastboot.request.protocol; - let host = this.fastboot.request.host; + const httpRegex = /^https?:\/\//; + const protocolRelativeRegex = /^\/\//; + const protocol = this.fastboot.request.protocol; + const host = this.fastboot.request.host; if (protocolRelativeRegex.test(url)) { return `${protocol}${url}`; @@ -1240,12 +1174,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {String} responseText @return {Object} */ - parseErrorResponse(responseText: string): Dict | string { + parseErrorResponse(responseText: string): Record | string { let json: string = responseText; try { json = JSON.parse(responseText); - } catch (e) { + } catch { // ignored } @@ -1260,7 +1194,11 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} payload @return {Array} errors payload */ - normalizeErrorResponse(status: number, _headers: Dict, payload: Payload): Dict[] { + normalizeErrorResponse( + status: number, + _headers: Record, + payload: Payload + ): Record[] { if (payload && typeof payload === 'object' && 'errors' in payload && Array.isArray(payload.errors)) { return payload.errors; } else { @@ -1290,18 +1228,25 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} requestData @return {String} detailed error message */ - generatedDetailedMessage(status: number, headers, payload: Payload, requestData: RequestData): string { + generatedDetailedMessage( + status: number, + headers: Record, + payload: Payload, + requestData: RequestData + ): string { let shortenedPayload; - let payloadContentType = headers['content-type'] || 'Empty Content-Type'; + const payloadContentType = headers['content-type'] || 'Empty Content-Type'; if (payloadContentType === 'text/html' && typeof payload === 'string' && payload.length > 250) { shortenedPayload = '[Omitted Lengthy HTML]'; + } else if (typeof payload === 'object' && payload !== null) { + shortenedPayload = JSON.stringify(payload, null, 2); } else { shortenedPayload = payload; } - let requestDescription = requestData.method + ' ' + requestData.url; - let payloadDescription = 'Payload (' + payloadContentType + ')'; + const requestDescription = requestData.method + ' ' + requestData.url; + const payloadDescription = 'Payload (' + payloadContentType + ')'; return [ 'Ember Data Request ' + requestDescription + ' returned a ' + status, @@ -1321,12 +1266,15 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @return {Object} */ buildQuery(snapshot: Snapshot | SnapshotRecordArray): QueryState { - let query: QueryState = {}; + const query: QueryState = {}; if (snapshot) { - let { include } = snapshot; + const { include } = snapshot; if (include) { + // note: if user passed in an array, this will serialize like `?include[]=foo&include[]=bar` + // but if user passed in a string, this will serialize like `?include=foo,bar` + // users that want consistent behavior should override this method query.include = include; } } @@ -1335,6 +1283,9 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { } } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface RESTAdapter extends MixtBuildURLMixin {} + function ajaxSuccess( adapter: RESTAdapter, payload: Payload, @@ -1345,10 +1296,12 @@ function ajaxSuccess( try { response = adapter.handleResponse(responseData.status, responseData.headers, payload, requestData); } catch (error) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors return Promise.reject(error); } if (response && response.isAdapterError) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors return Promise.reject(response); } else { return response; @@ -1360,7 +1313,7 @@ function ajaxError( payload: Payload, requestData: RequestData, responseData: ResponseData -): Error | TimeoutError | AbortError | Dict { +): Error | TimeoutError | AbortError | Record { let error; if (responseData.errorThrown instanceof Error && payload !== '') { @@ -1387,17 +1340,17 @@ function ajaxError( // Adapter abort error to include any relevent info, e.g. request/response: function handleAbort(requestData: RequestData, responseData: ResponseData): AbortError { - let { method, url, errorThrown } = requestData; - let { status } = responseData; - let msg = `Request failed: ${method} ${url} ${errorThrown || ''}`; - let errors = [{ title: 'Adapter Error', detail: msg.trim(), status }]; + const { method, url, errorThrown } = requestData; + const { status } = responseData; + const msg = `Request failed: ${method} ${url} ${String(errorThrown ?? '')}`; + const errors = [{ title: 'Adapter Error', detail: msg.trim(), status }]; return new AbortError(errors); } //From http://stackoverflow.com/questions/280634/endswith-in-javascript function endsWith(string: string, suffix: string): boolean { if (typeof String.prototype.endsWith !== 'function') { - return string.indexOf(suffix, string.length - suffix.length) !== -1; + return string.includes(suffix, string.length - suffix.length); } else { return string.endsWith(suffix); } @@ -1409,7 +1362,7 @@ function fetchSuccessHandler( response: Response, requestData: RequestData ): Promise { - let responseData = fetchResponseData(response); + const responseData = fetchResponseData(response); return ajaxSuccess(adapter, payload, requestData, responseData); } @@ -1419,11 +1372,12 @@ function fetchErrorHandler( response: Response, errorThrown, requestData: RequestData -) { - let responseData = fetchResponseData(response); +): Error | TimeoutError | Record { + const responseData = fetchResponseData(response); if (responseData.status === 200 && payload instanceof Error) { responseData.errorThrown = payload; + // @ts-expect-error poorly typed payload = responseData.errorThrown.payload; } else { responseData.errorThrown = errorThrown; @@ -1440,18 +1394,23 @@ function ajaxSuccessHandler( jqXHR: JQuery.jqXHR, requestData: RequestData ): Promise { - let responseData = ajaxResponseData(jqXHR); + const responseData = ajaxResponseData(jqXHR); return ajaxSuccess(adapter, payload, requestData, responseData); } -function ajaxErrorHandler(adapter: RESTAdapter, jqXHR: JQuery.jqXHR, errorThrown: string, requestData: RequestData) { - let responseData = ajaxResponseData(jqXHR); +function ajaxErrorHandler( + adapter: RESTAdapter, + jqXHR: JQuery.jqXHR, + errorThrown: Error | string, + requestData: RequestData +) { + const responseData = ajaxResponseData(jqXHR); responseData.errorThrown = errorThrown; - let payload = adapter.parseErrorResponse(jqXHR.responseText); + const payload = adapter.parseErrorResponse(jqXHR.responseText); if (DEBUG) { - let message = `The server returned an empty string for ${requestData.method} ${requestData.url}, which cannot be parsed into a valid JSON. Return either null or {}.`; - let validJSONString = !(responseData.textStatus === 'parsererror' && payload === ''); + const message = `The server returned an empty string for ${requestData.method} ${requestData.url}, which cannot be parsed into a valid JSON. Return either null or {}.`; + const validJSONString = !(responseData.textStatus === 'parsererror' && payload === ''); warn(message, validJSONString, { id: 'ds.adapter.returned-empty-string-as-JSON', }); @@ -1476,8 +1435,8 @@ function ajaxResponseData(jqXHR: JQuery.jqXHR): ResponseData { }; } -function headersToObject(headers: Headers): Dict { - let headersObject = {}; +function headersToObject(headers: Headers): Record { + const headersObject = {}; if (headers) { headers.forEach((value, key) => (headersObject[key] = value)); @@ -1494,7 +1453,7 @@ function headersToObject(headers: Headers): Dict { * @param {Object} _options * @param {Adapter} adapter * @private - * @returns {Object} + * @return {Object} */ export function fetchOptions( options: JQueryRequestInit & Partial, @@ -1508,7 +1467,7 @@ export function fetchOptions( // If no options are passed, Ember Data sets `data` to an empty object, which we test for. if (Object.keys(options.data).length && options.url) { // Test if there are already query params in the url (mimics jQuey.ajax). - const queryParamDelimiter = options.url.indexOf('?') > -1 ? '&' : '?'; + const queryParamDelimiter = options.url.includes('?') ? '&' : '?'; options.url += `${queryParamDelimiter}${serializeQueryParams(options.data)}`; } } else { @@ -1525,6 +1484,7 @@ export function fetchOptions( if (Object.prototype.toString.call(options.data) === '[object Object]') { options.body = JSON.stringify(options.data); } else { + // @ts-expect-error poorly typed options.body = options.data; } } @@ -1544,7 +1504,7 @@ function ajaxOptions(options: JQueryRequestInit, adapter: RESTAdapter): JQueryRe options.beforeSend = function (xhr) { if (options.headers) { Object.keys(options.headers).forEach((key) => { - let headerValue = options.headers && options.headers[key]; + const headerValue = options.headers && options.headers[key]; const isString = (value: unknown): value is string => typeof value === 'string'; if (isString(headerValue)) { xhr.setRequestHeader(key, headerValue); @@ -1556,4 +1516,54 @@ function ajaxOptions(options: JQueryRequestInit, adapter: RESTAdapter): JQueryRe return options; } +function execjQAjax( + adapter: RESTAdapter, + requestData: RequestData, + options: JQueryAjaxSettings +): Promise { + const hash = adapter.ajaxOptions(requestData.url, requestData.method, options) as JQueryRequestInit; + + return new Promise((resolve, reject) => { + hash.success = function (payload: Payload, textStatus, jqXHR) { + const response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); + resolve(response); + }; + + hash.error = function (jqXHR, textStatus, errorThrown: Error | string) { + const error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(error); + }; + + adapter._ajax(hash); + }); +} + +function splitGroupToFitInUrl( + store: Store, + adapter: RESTAdapter, + group: Snapshot[], + maxURLLength: number, + paramNameLength: number +) { + let idsSize = 0; + const baseUrl = adapter._stripIDFromURL(store, group[0]); + const splitGroups: Snapshot[][] = [[]]; + + group.forEach((snapshot) => { + const additionalLength = encodeURIComponent(snapshot.id!).length + paramNameLength; + if (baseUrl.length + idsSize + additionalLength >= maxURLLength) { + idsSize = 0; + splitGroups.push([]); + } + + idsSize += additionalLength; + + const lastGroupIndex = splitGroups.length - 1; + splitGroups[lastGroupIndex].push(snapshot); + }); + + return splitGroups; +} + export default RESTAdapter; diff --git a/packages/adapter/tsconfig.json b/packages/adapter/tsconfig.json new file mode 100644 index 00000000000..99664283962 --- /dev/null +++ b/packages/adapter/tsconfig.json @@ -0,0 +1,83 @@ +{ + "include": ["src/**/*", "../../@types/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "noImplicitAny": false, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types", "@types/jquery"], + "paths": { + "@ember-data/graph": ["../graph/unstable-preview-types"], + "@ember-data/graph/*": ["../graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../legacy-compat/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../graph" + }, + { + "path": "../json-api" + }, + { + "path": "../legacy-compat" + }, + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/adapter/vite.config.mjs b/packages/adapter/vite.config.mjs new file mode 100644 index 00000000000..d8de6399750 --- /dev/null +++ b/packages/adapter/vite.config.mjs @@ -0,0 +1,24 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember/service', // inject the store to base Adapter + '@ember/debug', // assert, deprecate + '@ember/object', // Adapter base, computed for headers + '@ember/object/mixin', // BuildURLMixin + '@ember/application', // getOwner +]; +export const entryPoints = [ + './src/index.ts', + './src/error.js', + './src/json-api.ts', + './src/rest.ts', + './src/-private.ts', +]; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/build-config/CHANGELOG.md b/packages/build-config/CHANGELOG.md new file mode 100644 index 00000000000..ac5109dbe97 --- /dev/null +++ b/packages/build-config/CHANGELOG.md @@ -0,0 +1,26 @@ +# @warp-drive/build-config Changelog + +## v0.0.0-alpha.22 (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)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9455](https://github.com/emberjs/data/pull/9455) fix: config version lookup needs to be project location aware ([@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)) +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + diff --git a/packages/build-config/LICENSE.md b/packages/build-config/LICENSE.md new file mode 100644 index 00000000000..ee1ae5bf425 --- /dev/null +++ b/packages/build-config/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (C) 2023 EmberData and WarpDrive contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/build-config/NCC-1701-a-blue.svg b/packages/build-config/NCC-1701-a-blue.svg new file mode 100644 index 00000000000..3b46f232c1a --- /dev/null +++ b/packages/build-config/NCC-1701-a-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/build-config/NCC-1701-a.svg b/packages/build-config/NCC-1701-a.svg new file mode 100644 index 00000000000..8ee688dcf30 --- /dev/null +++ b/packages/build-config/NCC-1701-a.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/build-config/README.md b/packages/build-config/README.md new file mode 100644 index 00000000000..f61f0efbf17 --- /dev/null +++ b/packages/build-config/README.md @@ -0,0 +1,120 @@ +

+ + +

+ +

🛠️ @warp-drive/build-config

+

Enables providing a build config to optimize application assets

+ +## Installation + +```cli +pnpm install @warp-drive/build-config +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/build-config/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40warp-drive/build-config/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40warp-drive/build-config/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40warp-drive/build-config/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40warp-drive/build-config/lts-4-12?label=%40lts-4-12&color=bbbbbb) + +## Usage + +```ts +import { setConfig } from '@warp-drive/build-config'; + +setConfig(app, __dirname, { + // ... options +}); +``` + +In an ember-cli-build file that'll typically look like this: + +```ts +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + + const app = new EmberApp(defaults, {}); + + setConfig(app, __dirname, { + // WarpDrive/EmberData settings go here (if any) + }); + + return app.toTree(); +}; + +``` + +### ♥️ Credits + +
+ Brought to you with ♥️ love by 🐹 Ember + + +
diff --git a/packages/build-config/babel.config.mjs b/packages/build-config/babel.config.mjs new file mode 100644 index 00000000000..eba0be86353 --- /dev/null +++ b/packages/build-config/babel.config.mjs @@ -0,0 +1,3 @@ +export default { + plugins: [['@babel/plugin-transform-typescript', { allowDeclareFields: true }]], +}; diff --git a/packages/build-config/cjs-src/addon-shim.js b/packages/build-config/cjs-src/addon-shim.js new file mode 100644 index 00000000000..6dacdb70d4c --- /dev/null +++ b/packages/build-config/cjs-src/addon-shim.js @@ -0,0 +1,26 @@ +'use strict'; + +export function addonShim(dirName, options) { + const path = require('path'); + const pkg = require(path.join(dirName, './package.json')); + + const isV2Addon = pkg['ember-addon']?.version === 2; + if (isV2Addon) { + const { addonV1Shim } = require('@embroider/addon-shim'); + return addonV1Shim(dirName, options); + } + + const Funnel = require('broccoli-funnel'); + return { + name: pkg.name, + + treeForVendor() {}, + treeForPublic() {}, + treeForStyles() {}, + treeForAddonStyles() {}, + treeForApp() {}, + treeForAddon() { + return this._super.treeForAddon.call(this, new Funnel(path.join(dirName, 'dist'))); + }, + }; +} diff --git a/packages/build-config/cjs-src/transforms/babel-plugin-transform-asserts.js b/packages/build-config/cjs-src/transforms/babel-plugin-transform-asserts.js new file mode 100644 index 00000000000..d04abc4b07d --- /dev/null +++ b/packages/build-config/cjs-src/transforms/babel-plugin-transform-asserts.js @@ -0,0 +1,113 @@ +const { ImportUtil } = require('babel-import-util'); + +const Utils = new Set(['assert']); + +/* +// Before +import { assert } from '@warp-drive/build-config/macros'; + +assert('foo', true); + +// After +(macroCondition(isDevelopingApp()) ? function assert(test) { if (!test) { throw new Error('foo'); } }(true) : {}); +*/ + +// => _macros.getGlobalConfig().WarpDrive.env.DEBUG +function buildMacroConstDEBUG(types, binding, state) { + return types.memberExpression( + types.memberExpression( + types.memberExpression( + types.callExpression(state.importer.import(binding, '@embroider/macros', 'getGlobalConfig'), []), + types.identifier('WarpDrive') + ), + types.identifier('env') + ), + types.identifier('DEBUG') + ); +} + +// => _macros.macroCondition(_macros.getGlobalConfig().WarpDrive.env.DEBUG) +function buildMacroConditionDEBUG(types, binding, state) { + return types.callExpression(state.importer.import(binding, '@embroider/macros', 'macroCondition'), [ + buildMacroConstDEBUG(types, binding, state), + ]); +} + +// (test) => { if (!test) { throw new Error(someMessage); } }(someCond) +function buildAssert(types, originalCallExpression) { + const desc = originalCallExpression.arguments[0]; + const test = originalCallExpression.arguments[1] ?? types.booleanLiteral(false); + // prettier-ignore + return types.callExpression( + types.arrowFunctionExpression([types.identifier('test')], // (test) => + types.blockStatement([ // { + types.ifStatement( // if + types.unaryExpression('!', types.identifier('test')), // (!test) + types.blockStatement([ // { + types.throwStatement( // throw + types.newExpression(types.identifier('Error'), [desc]) // new Error(desc) + )]) // } + )]) // } + ), + [test] // (someCond) + ); +} + +// => ( ? : {}); +function buildAssertTernary(types, binding, state, originalCallExpression) { + return types.expressionStatement( + types.conditionalExpression( + buildMacroConditionDEBUG(types, binding, state), + buildAssert(types, originalCallExpression), + types.objectExpression([]) + ) + ); +} + +export default function (babel) { + const { types: t } = babel; + + return { + name: 'ast-transform', // not required + visitor: { + ImportDeclaration(path, state) { + const importPath = path.node.source.value; + + if (importPath === '@warp-drive/build-config/macros') { + const specifiers = path.get('specifiers'); + + specifiers.forEach((specifier) => { + const name = specifier.node.imported.name; + if (!Utils.has(name)) { + throw new Error(`Unexpected import '${name}' imported from '@warp-drive/build-config/macros'`); + } + + const localBindingName = specifier.node.local.name; + const binding = specifier.scope.getBinding(localBindingName); + + binding.referencePaths.forEach((p) => { + const originalCallExpression = p.parentPath.node; + + if (!t.isCallExpression(originalCallExpression)) { + throw new Error('Expected a call expression'); + } + + const assertTernary = buildAssertTernary(t, binding, state, originalCallExpression); + p.parentPath.replaceWith(assertTernary); + }); + specifier.scope.removeOwnBinding(localBindingName); + specifier.remove(); + }); + + if (path.get('specifiers').length === 0) { + path.remove(); + } + } + }, + + Program(path, state) { + state.importer = new ImportUtil(t, path); + }, + }, + }; +} diff --git a/packages/build-config/cjs-src/transforms/babel-plugin-transform-deprecations.js b/packages/build-config/cjs-src/transforms/babel-plugin-transform-deprecations.js new file mode 100644 index 00000000000..12c89f99fcd --- /dev/null +++ b/packages/build-config/cjs-src/transforms/babel-plugin-transform-deprecations.js @@ -0,0 +1,84 @@ +import { ImportUtil } from 'babel-import-util'; + +function parentIsUnary(node) { + if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { + return true; + } + return false; +} + +export default function (babel) { + const { types: t } = babel; + + return { + name: 'deprecation-flags', + visitor: { + ImportDeclaration(path, state) { + const importPath = path.node.source.value; + + if (importPath === state.opts.source) { + const specifiers = path.get('specifiers'); + specifiers.forEach((specifier) => { + let name = specifier.node.imported.name; + if (!(name in state.opts.flags)) { + throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); + } + let localBindingName = specifier.node.local.name; + let binding = specifier.scope.getBinding(localBindingName); + binding.referencePaths.forEach((p, other) => { + let negateStatement = false; + let node = p; + if (parentIsUnary(p)) { + negateStatement = true; + node = p.parentPath; + } + const comments = + node.node.leadingComments ?? + (node.parent.type === 'ConditionalExpression' && node.parent.leadingComments) ?? + []; + let shouldInlineConfigValue = false; + if (comments?.length) { + const lastComment = comments.at(-1); + if (lastComment.value.trim() === 'inline-macro-config') { + shouldInlineConfigValue = true; + } + } + + let getConfig = t.memberExpression( + t.memberExpression( + t.memberExpression( + t.callExpression(state.importer.import(p, '@embroider/macros', 'getGlobalConfig'), []), + t.identifier('WarpDrive') + ), + t.identifier('deprecations') + ), + t.identifier(name) + ); + + const configExp = negateStatement ? t.unaryExpression('!', getConfig) : getConfig; + const replaceExp = shouldInlineConfigValue + ? // if (DEPRECATE_FOO) + // => + // if (getGlobalConfig('WarpDrive').deprecations.FOO) + configExp + : // if (DEPRECATE_FOO) + // => + // if (macroCondition(getGlobalConfig('WarpDrive').deprecations.FOO)) + t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [configExp]); + node.replaceWith(replaceExp); + }); + specifier.scope.removeOwnBinding(localBindingName); + specifier.remove(); + }); + if (path.get('specifiers').length === 0) { + path.remove(); + } + } + }, + + Program(path, state) { + state.importer = new ImportUtil(t, path); + }, + }, + }; +} diff --git a/packages/build-config/cjs-src/transforms/babel-plugin-transform-features.js b/packages/build-config/cjs-src/transforms/babel-plugin-transform-features.js new file mode 100644 index 00000000000..0b0201a505d --- /dev/null +++ b/packages/build-config/cjs-src/transforms/babel-plugin-transform-features.js @@ -0,0 +1,78 @@ +import { ImportUtil } from 'babel-import-util'; +import fs from 'fs'; + +const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')); +const version = pkg.version; + +const isCanary = version.includes('alpha'); + +function parentIsUnary(node) { + if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { + return true; + } + return false; +} + +export default function (babel) { + const { types: t } = babel; + + return { + name: 'ast-transform', // not required + visitor: { + ImportDeclaration(path, state) { + const importPath = path.node.source.value; + + if (importPath === state.opts.source) { + const specifiers = path.get('specifiers'); + specifiers.forEach((specifier) => { + let name = specifier.node.imported.name; + if (!(name in state.opts.flags)) { + throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); + } + let localBindingName = specifier.node.local.name; + let binding = specifier.scope.getBinding(localBindingName); + binding.referencePaths.forEach((p) => { + let negateStatement = false; + let node = p; + if (!isCanary) { + p.replaceWith(t.boolean(state.opts.flags[name])); + return; + } + if (parentIsUnary(p)) { + negateStatement = true; + node = p.parentPath; + } + let getConfig = t.memberExpression( + t.memberExpression( + t.memberExpression( + t.callExpression(state.importer.import(p, '@embroider/macros', 'getGlobalConfig'), []), + t.identifier('WarpDrive') + ), + t.identifier('features') + ), + t.identifier(name) + ); + node.replaceWith( + // if (LOG_FOO) + // => + // if (macroCondition(getGlobalConfig('WarpDrive').debug.LOG_FOO)) + t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ + negateStatement ? t.unaryExpression('!', getConfig) : getConfig, + ]) + ); + }); + specifier.scope.removeOwnBinding(localBindingName); + specifier.remove(); + }); + if (path.get('specifiers').length === 0) { + path.remove(); + } + } + }, + + Program(path, state) { + state.importer = new ImportUtil(t, path); + }, + }, + }; +} diff --git a/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js b/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js new file mode 100644 index 00000000000..c7645039673 --- /dev/null +++ b/packages/build-config/cjs-src/transforms/babel-plugin-transform-logging.js @@ -0,0 +1,68 @@ +import { ImportUtil } from 'babel-import-util'; + +function parentIsUnary(node) { + if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { + return true; + } + return false; +} + +export default function (babel) { + const { types: t } = babel; + + return { + name: 'ast-transform', // not required + visitor: { + ImportDeclaration(path, state) { + const importPath = path.node.source.value; + + if (importPath === state.opts.source) { + const specifiers = path.get('specifiers'); + specifiers.forEach((specifier) => { + let name = specifier.node.imported.name; + if (!(name in state.opts.flags)) { + throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); + } + let localBindingName = specifier.node.local.name; + let binding = specifier.scope.getBinding(localBindingName); + binding.referencePaths.forEach((p) => { + let negateStatement = false; + let node = p; + if (parentIsUnary(p)) { + negateStatement = true; + node = p.parentPath; + } + let getConfig = t.memberExpression( + t.memberExpression( + t.memberExpression( + t.callExpression(state.importer.import(p, '@embroider/macros', 'getGlobalConfig'), []), + t.identifier('WarpDrive') + ), + t.identifier(state.opts.configKey) + ), + t.identifier(name) + ); + node.replaceWith( + // if (LOG_FOO) + // => + // if (macroCondition(getGlobalConfig('WarpDrive').debug.LOG_FOO)) + t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ + negateStatement ? t.unaryExpression('!', getConfig) : getConfig, + ]) + ); + }); + specifier.scope.removeOwnBinding(localBindingName); + specifier.remove(); + }); + if (path.get('specifiers').length === 0) { + path.remove(); + } + } + }, + + Program(path, state) { + state.importer = new ImportUtil(t, path); + }, + }, + }; +} diff --git a/packages/build-config/package.json b/packages/build-config/package.json new file mode 100644 index 00000000000..297950862a9 --- /dev/null +++ b/packages/build-config/package.json @@ -0,0 +1,69 @@ +{ + "name": "@warp-drive/build-config", + "version": "4.12.8", + "description": "Provides Build Configuration for projects using WarpDrive or EmberData", + "keywords": [ + "ember-data", + "warp-drive" + ], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/build-config" + }, + "license": "MIT", + "author": "Chris Thoburn ", + "scripts": { + "build:infra": "vite build; vite build -c ./vite.config-cjs.mjs;", + "prepack": "bun run build:infra" + }, + "type": "module", + "files": [ + "dist", + "unstable-preview-types", + "CHANGELOG.md", + "README.md", + "LICENSE.md", + "NCC-1701-a.svg", + "NCC-1701-a-blue.svg" + ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*.cjs": { + "default": "./dist/*.cjs" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, + "dependencies": { + "@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" + }, + "devDependencies": { + "@warp-drive/internal-config": "workspace:*", + "@types/babel__core": "^7.20.5", + "@types/node": "^20.14.2", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@babel/core": "^7.24.5", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "bun-types": "^1.1.30", + "vite": "^5.2.11" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9" +} diff --git a/packages/build-config/src/-private/utils/deprecations.ts b/packages/build-config/src/-private/utils/deprecations.ts new file mode 100644 index 00000000000..621b68e0c83 --- /dev/null +++ b/packages/build-config/src/-private/utils/deprecations.ts @@ -0,0 +1,55 @@ +import semver from 'semver'; + +import * as CURRENT_DEPRECATIONS from '../../deprecation-versions.ts'; +type MajorMinor = `${number}.${number}`; +type DeprecationFlag = keyof typeof CURRENT_DEPRECATIONS; + +function deprecationIsResolved(deprecatedSince: MajorMinor, compatVersion: MajorMinor) { + return semver.lte(semver.minVersion(deprecatedSince)!, semver.minVersion(compatVersion)!); +} + +const NextMajorVersion = '5.'; + +function deprecationIsNextMajorCycle(deprecatedSince: MajorMinor) { + return deprecatedSince.startsWith(NextMajorVersion); +} + +export function getDeprecations( + compatVersion: MajorMinor | null | undefined, + deprecations?: { [key in DeprecationFlag]?: boolean } +): { [key in DeprecationFlag]: boolean } { + const flags = {} as Record; + const keys = Object.keys(CURRENT_DEPRECATIONS) as DeprecationFlag[]; + const DISABLE_6X_DEPRECATIONS = deprecations?.DISABLE_6X_DEPRECATIONS ?? true; + + keys.forEach((flag) => { + const deprecatedSince = CURRENT_DEPRECATIONS[flag]; + 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]; + } else if (compatVersion) { + // if we are told we are compatible with a version + // we check if we can strip this flag + const isResolved = deprecationIsResolved(deprecatedSince, compatVersion); + // if we've resolved, we strip (by setting the flag to false) + /* + if (DEPRECATED_FEATURE) { + // deprecated code path + } else { + // if needed a non-deprecated code path + } + */ + flagState = !isResolved; + } + } + + // console.log(`${flag}=${flagState} (${deprecatedSince} <= ${compatVersion})`); + flags[flag] = flagState; + }); + + return flags; +} diff --git a/packages/build-config/src/-private/utils/features.ts b/packages/build-config/src/-private/utils/features.ts new file mode 100644 index 00000000000..eb930a15e71 --- /dev/null +++ b/packages/build-config/src/-private/utils/features.ts @@ -0,0 +1,72 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import * as CURRENT_FEATURES from '../../canary-features.ts'; +type FEATURE = keyof typeof CURRENT_FEATURES; + +const dirname = typeof __dirname !== 'undefined' ? __dirname : fileURLToPath(new URL('.', import.meta.url)); +const relativePkgPath = path.join(dirname, '../package.json'); + +const version = JSON.parse(fs.readFileSync(relativePkgPath, 'utf-8')).version; +const isCanary = version.includes('alpha'); + +export function getFeatures(isProd: boolean): { [key in FEATURE]: boolean } { + const features = Object.assign({}, CURRENT_FEATURES) as Record; + const keys = Object.keys(features) as FEATURE[]; + + if (!isCanary) { + // disable all features with a current value of `null` + for (const feature of keys) { + let featureValue = features[feature]; + + if (featureValue === null) { + features[feature] = false; + } + } + return features; + } + + const FEATURE_OVERRIDES = process.env.EMBER_DATA_FEATURE_OVERRIDE; + if (FEATURE_OVERRIDES === 'ENABLE_ALL_OPTIONAL') { + // enable all features with a current value of `null` + for (const feature of keys) { + let featureValue = features[feature]; + + if (featureValue === null) { + features[feature] = true; + } + } + } else if (FEATURE_OVERRIDES === 'DISABLE_ALL') { + // disable all features, including those with a value of `true` + for (const feature of keys) { + features[feature] = false; + } + } else if (FEATURE_OVERRIDES) { + // enable only the specific features listed in the environment + // variable (comma separated) + const forcedFeatures = FEATURE_OVERRIDES.split(','); + for (let i = 0; i < forcedFeatures.length; i++) { + let featureName = forcedFeatures[i]; + + if (!keys.includes(featureName as FEATURE)) { + throw new Error(`Unknown feature flag: ${featureName}`); + } + + features[featureName as FEATURE] = true; + } + } + + if (isProd) { + // disable all features with a current value of `null` + for (const feature of keys) { + let featureValue = features[feature]; + + if (featureValue === null) { + features[feature] = false; + } + } + } + + return features; +} diff --git a/packages/build-config/src/-private/utils/get-env.ts b/packages/build-config/src/-private/utils/get-env.ts new file mode 100644 index 00000000000..35cc5ff8fab --- /dev/null +++ b/packages/build-config/src/-private/utils/get-env.ts @@ -0,0 +1,16 @@ +export function getEnv() { + const { EMBER_ENV, IS_TESTING, EMBER_CLI_TEST_COMMAND, NODE_ENV, CI, IS_RECORDING } = process.env; + const PRODUCTION = EMBER_ENV === 'production' || (!EMBER_ENV && NODE_ENV === 'production'); + const DEBUG = !PRODUCTION; + const TESTING = DEBUG || Boolean(EMBER_ENV === 'test' || IS_TESTING || EMBER_CLI_TEST_COMMAND); + const SHOULD_RECORD = Boolean(!CI || IS_RECORDING); + + return { + TESTING, + PRODUCTION, + DEBUG, + IS_RECORDING: Boolean(IS_RECORDING), + IS_CI: Boolean(CI), + SHOULD_RECORD, + }; +} diff --git a/packages/build-config/src/babel-macros.ts b/packages/build-config/src/babel-macros.ts new file mode 100644 index 00000000000..df79474b854 --- /dev/null +++ b/packages/build-config/src/babel-macros.ts @@ -0,0 +1,73 @@ +import * as LOGGING from './debugging.ts'; +import * as CURRENT_FEATURES from './canary-features.ts'; +import * as CURRENT_DEPRECATIONS from './deprecations.ts'; + +type FEATURE = keyof typeof CURRENT_FEATURES; +const features = Object.keys(CURRENT_FEATURES) as FEATURE[]; +const FEATURES = Object.assign({}, CURRENT_FEATURES) as Record; +features.forEach((feature) => { + let featureValue = FEATURES[feature]; + if (featureValue === null) { + FEATURES[feature] = false; + } +}); + +const config = { + features: FEATURES, + deprecations: Object.assign({}, CURRENT_DEPRECATIONS), + debug: Object.assign({}, LOGGING), +}; + +export function macros() { + const TransformAsserts = import.meta.resolve('./babel-plugin-transform-asserts.cjs').slice(7); + const TransformDeprecations = import.meta.resolve('./babel-plugin-transform-deprecations.cjs').slice(7); + const TransformDebugLogging = import.meta.resolve('./babel-plugin-transform-logging.cjs').slice(7); + const TransformFeatures = import.meta.resolve('./babel-plugin-transform-features.cjs').slice(7); + + let plugins = [ + [TransformAsserts, {}, '@warp-drive/build-config/asserts-stripping'], + [ + TransformFeatures, + { + source: '@warp-drive/build-config/canary-features', + flags: config.features, + }, + '@warp-drive/build-config/canary-features-stripping', + ], + [ + TransformDeprecations, + { + source: '@warp-drive/build-config/deprecations', + flags: config.deprecations, + }, + '@warp-drive/build-config/deprecation-stripping', + ], + [ + TransformDebugLogging, + { + source: '@warp-drive/build-config/debugging', + configKey: 'debug', + flags: config.debug, + }, + '@warp-drive/build-config/debugging-stripping', + ], + [ + TransformDebugLogging, + { + source: '@warp-drive/build-config/env', + configKey: 'env', + flags: { + TESTING: true, + PRODUCTION: true, + DEBUG: true, + IS_RECORDING: true, + IS_CI: true, + SHOULD_RECORD: true, + }, + }, + '@warp-drive/build-config/env', + ], + ]; + + return plugins; +} diff --git a/packages/build-config/src/canary-features.ts b/packages/build-config/src/canary-features.ts new file mode 100644 index 00000000000..bb3c93e36b6 --- /dev/null +++ b/packages/build-config/src/canary-features.ts @@ -0,0 +1,89 @@ +/** + * ## Canary Features + * + * EmberData allows users to test features that are implemented but not yet + * available even in canary. + * + * Typically these features represent work that might introduce a new concept, + * new API, change an API, or risk an unintended change in behavior to consuming + * applications. + * + * Such features have their implementations guarded by a "feature flag", and the + * flag is only activated once the core-data team is prepared to ship the work + * in a canary release. + * + * ### Installing Canary + * + * To test a feature you MUST be using a canary build. Canary builds are published + * to `npm` and can be installed using a precise tag (such as `ember-data@3.16.0-alpha.1`) + * or by installing the latest dist-tag published to the `canary` channel using your javascript + * package manager of choice. For instance with [pnpm](https://pnpm.io/) + + ```cli + pnpm add ember-data@canary + ``` + * + * ### Activating a Canary Feature + * + * Once you have installed canary, feature-flags can be activated at build-time + * + * by setting an environment variable: + * + * ```cli + * # Activate a single flag + * EMBER_DATA_FEATURE_OVERRIDE=SOME_FLAG ember build + * + * # Activate multiple flags by separating with commas + * EMBER_DATA_FEATURE_OVERRIDE=SOME_FLAG,OTHER_FLAG ember build + * + * # Activate all flags + * EMBER_DATA_FEATURE_OVERRIDE=ENABLE_ALL_OPTIONAL ember build + * ``` + * + * or by setting the appropriate flag in your `ember-cli-build` file: + * + * ```ts + * let app = new EmberApp(defaults, { + * emberData: { + * features: { + * SAMPLE_FEATURE_FLAG: false // utliize existing behavior, strip code for the new feature + * OTHER_FEATURE_FLAG: true // utilize this new feature, strip code for the older behavior + * } + * } + * }) + * ``` + * + * **The "off" branch of feature-flagged code is always stripped from production builds.** + * + * The list of available feature-flags is located [here](https://github.com/emberjs/data/tree/main/packages/build-config/src/virtual/canary-features.ts "List of EmberData FeatureFlags") + * + * + * ### Preparing a Project to use a Canary Feature + * + * For most projects, simple version detection should be enough. + * Using the provided version compatibility helpers from [embroider-macros](https://github.com/embroider-build/embroider/tree/main/packages/macros#readme) + * the following can be done: + * + * ```js + * if (macroCondition(dependencySatisfies('@ember-data/store', '5.0'))) { + * // do thing + * } + * ``` + * + @module @warp-drive/build-config/canary-features + @main @warp-drive/build-config/canary-features + */ +/** + This is the current list of features used at build time for canary releases. + If empty there are no features currently gated by feature flags. + + The valid values are: + + - `true` | The feature is **enabled** at all times, and cannot be disabled. + - `false` | The feature is **disabled** at all times, and cannot be enabled. + - `null` | The feature is **disabled by default**, but can be enabled via configuration. + + @class CanaryFeatureFlags + @public +*/ +export const SAMPLE_FEATURE_FLAG: boolean | null = null; diff --git a/packages/build-config/src/cjs-set-config.ts b/packages/build-config/src/cjs-set-config.ts new file mode 100644 index 00000000000..d6c2f7e5d74 --- /dev/null +++ b/packages/build-config/src/cjs-set-config.ts @@ -0,0 +1 @@ +export { setConfig } from './index.ts'; diff --git a/packages/build-config/src/debugging.ts b/packages/build-config/src/debugging.ts new file mode 100644 index 00000000000..60d6a2880c5 --- /dev/null +++ b/packages/build-config/src/debugging.ts @@ -0,0 +1,105 @@ +/** + * ## Debugging + * + * Many portions of the internals are helpfully instrumented with logging that can be activated + * at build time. This instrumentation is always removed from production builds or any builds + * that has not explicitly activated it. To activate it set the appropriate flag to `true`. + * + @module @warp-drive/build-config/debugging + @main @warp-drive/build-config/debugging + */ +/** + * + * Many portions of the internals are helpfully instrumented with logging that can be activated +at build time. This instrumentation is always removed from production builds or any builds +that has not explicitly activated it. To activate it set the appropriate flag to `true`. + +```ts + let app = new EmberApp(defaults, { + emberData: { + debug: { + LOG_PAYLOADS: false, // data store received to update cache with + LOG_OPERATIONS: false, // updates to cache remote state + LOG_MUTATIONS: false, // updates to cache local state + LOG_NOTIFICATIONS: false, + LOG_REQUESTS: false, + LOG_REQUEST_STATUS: false, + LOG_IDENTIFIERS: false, + LOG_GRAPH: false, + LOG_INSTANCE_CACHE: false, + } + } + }); + ``` + + @class DebugLogging + @public + */ +/** + * log payloads received by the store + * via `push` or returned from a delete/update/create + * operation. + * + * @property {boolean} LOG_PAYLOADS + * @public + */ +export const LOG_PAYLOADS: boolean = false; +/** + * log remote-state updates to the cache + * + * @property {boolean} LOG_OPERATIONS + * @public + */ +export const LOG_OPERATIONS: boolean = false; +/** + * log local-state updates to the cache + * + * @property {boolean} LOG_MUTATIONS + * @public + */ +export const LOG_MUTATIONS: boolean = false; +/** + * log notifications received by the NotificationManager + * + * @property {boolean} LOG_NOTIFICATIONS + * @public + */ +export const LOG_NOTIFICATIONS: boolean = false; +/** + * log requests issued by the RequestManager + * + * @property {boolean} LOG_REQUESTS + * @public + */ +export const LOG_REQUESTS: boolean = false; +/** + * log updates to requests the store has issued to + * the network (adapter) to fulfill. + * + * @property {boolean} LOG_REQUEST_STATUS + * @public + */ +export const LOG_REQUEST_STATUS: boolean = false; +/** + * log peek, generation and updates to + * Record Identifiers. + * + * @property {boolean} LOG_IDENTIFIERS + * @public + */ +export const LOG_IDENTIFIERS: boolean = false; +/** + * log updates received by the graph (relationship pointer storage) + * + * @property {boolean} LOG_GRAPH + * @public + */ +export const LOG_GRAPH: boolean = false; +/** + * log creation/removal of RecordData and Record + * instances. + * + * @property {boolean} LOG_INSTANCE_CACHE + * @public + */ +export const LOG_INSTANCE_CACHE: boolean = false; diff --git a/packages/build-config/src/deprecation-versions.ts b/packages/build-config/src/deprecation-versions.ts new file mode 100644 index 00000000000..80d5148b4d3 --- /dev/null +++ b/packages/build-config/src/deprecation-versions.ts @@ -0,0 +1,1040 @@ +// ======================== +// FOR CONTRIBUTING AUTHORS +// +// Deprecations here should also have guides PR'd to the emberjs deprecation app +// +// github: https://github.com/ember-learn/deprecation-app +// website: https://deprecations.emberjs.com +// +// Each deprecation should also be given an associated URL pointing to the +// relevant guide. +// +// URLs should be of the form: https://deprecations.emberjs.com/v.x#toc_ +// where is the major version of the deprecation and is the +// name of the markdown file in the guides repo. +// +// ======================== +// + +/** + * ## Deprecations + * + * EmberData allows users to opt-in and remove code that exists to support deprecated + * behaviors. + * + * If your app has resolved all deprecations present in a given version, + * you may specify that version as your "compatibility" version to remove + * the code that supported the deprecated behavior from your app. + * + * For instance, if a deprecation was introduced in 3.13, and the app specifies + * 3.13 as its minimum version compatibility, any deprecations introduced before + * or during 3.13 would be stripped away. + * + * An app can use a different version than what it specifies as it's compatibility + * version. For instance, an App could be using `3.16` while specifying compatibility + * with `3.12`. This would remove any deprecations that were present in or before `3.12` + * but keep support for anything deprecated in or above `3.13`. + * + * ### Configuring Compatibility + * + * To configure your compatibility version, set the `compatWith` to the version you + * are compatible with on the `emberData` config in your `ember-cli-build.js` file. + * + * ```js + * const { setConfig } = await import('@warp-drive/build-config'); + * + * let app = new EmberApp(defaults, {}); + * + * setConfig(app, __dirname, { compatWith: '3.12' }); + * ``` + * + * Alternatively, individual deprecations can be resolved (and thus have its support stripped) + * via one of the flag names listed below. For instance, given a flag named `DEPRECATE_FOO_BEHAVIOR`. + * + * This capability is interopable with `compatWith`. You may set `compatWith` and then selectively resolve + * additional deprecations, or set compatWith and selectively un-resolve specific deprecations. + * + * Note: EmberData does not test against permutations of deprecations being stripped, our tests run against + * "all deprecated code included" and "all deprecated code removed". Unspecified behavior may sometimes occur + * when removing code for only some deprecations associated to a version number. + * + * ```js + * const { setConfig } = await import('@warp-drive/build-config'); + * + * let app = new EmberApp(defaults, {}); + * + * setConfig(app, __dirname, { + * deprecations: { + * DEPRECATE_FOO_BEHAVIOR: false // set to false to strip this code + * DEPRECATE_BAR_BEHAVIOR: true // force to true to not strip this code + * } + * }); + * ``` + * + * The complete list of which versions specific deprecations will be removed in + * can be found [here](https://github.com/emberjs/data/blob/main/packages/build-config/src/virtual/deprecation-versions.ts "List of EmberData Deprecations") + * + * @module @warp-drive/build-config/deprecations + * @main @warp-drive/build-config/deprecations + */ + +/** + * The following list represents deprecations currently active. + * + * Some deprecation flags guard multiple deprecation IDs. All + * associated IDs are listed. + * + * @class CurrentDeprecations + * @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** + * + * Currently, EmberData expects that the `type` property associated with + * a resource follows several conventions. + * + * - The `type` property must be a non-empty string + * - The `type` property must be singular + * - The `type` property must be dasherized + * + * We are deprecating support for types that do not match this pattern + * in order to unlock future improvements in which we can support `type` + * being any string of your choosing. + * + * The goal is that in the future, you will be able to use any string + * so long as it matches what your configured cache, identifier generation, + * and schemas expect. + * + * E.G. It will matter not that your string is in a specific format like + * singular, dasherized, etc. so long as everywhere you refer to the type + * you use the same string. + * + * If using @ember-data/model, there will always be a restriction that the + * `type` must match the path on disk where the model is defined. + * + * e.g. `app/models/foo/bar-bem.js` must have a type of `foo/bar-bem` + * + * @property DEPRECATE_NON_STRICT_TYPES + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_NON_STRICT_TYPES = '5.3'; + +/** + * **id: ember-data:deprecate-non-strict-id** + * + * Currently, EmberData expects that the `id` property associated with + * a resource is a string. + * + * However, for legacy support in many locations we would accept a number + * which would then immediately be coerced into a string. + * + * We are deprecating this legacy support for numeric IDs. + * + * The goal is that in the future, you will be able to use any ID format + * so long as everywhere you refer to the ID you use the same format. + * + * However, for identifiers we will always use string IDs and so any + * custom identifier configuration should provide a string ID. + * + * @property DEPRECATE_NON_STRICT_ID + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_NON_STRICT_ID = '5.3'; + +/** + * **id: ember-data:deprecate-non-unique-collection-payloads** + * + * Deprecates when the data for a hasMany relationship contains + * duplicate identifiers. + * + * Previously, relationships would silently de-dupe the data + * when received, but this behavior is being removed in favor + * of erroring if the same related record is included multiple + * times. + * + * For instance, in JSON:API the below relationship data would + * be considered invalid: + * + * ```json + * { + * "data": { + * "type": "article", + * "id": "1", + * "relationships": { + * "comments": { + * "data": [ + * { "type": "comment", "id": "1" }, + * { "type": "comment", "id": "2" }, + * { "type": "comment", "id": "1" } // duplicate + * ] + * } + * } + * } + * ``` + * + * To resolve this deprecation, either update your server to + * not include duplicate data, or implement normalization logic + * in either a request handler or serializer which removes + * duplicate data from relationship payloads. + * + * @property DEPRECATE_NON_UNIQUE_PAYLOADS + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_NON_UNIQUE_PAYLOADS = '5.3'; + +/** + * **id: ember-data:deprecate-relationship-remote-update-clearing-local-state** + * + * Deprecates when a relationship is updated remotely and the local state + * is cleared of all changes except for "new" records. + * + * Instead, any records not present in the new payload will be considered + * "removed" while any records present in the new payload will be considered "added". + * + * This allows us to "commit" local additions and removals, preserving any additions + * or removals that are not yet reflected in the remote state. + * + * For instance, given the following initial state: + * + * remote: A, B, C + * local: add D, E + * remove B, C + * => A, D, E + * + * + * If after an update, the remote state is now A, B, D, F then the new state will be + * + * remote: A, B, D, F + * local: add E + * remove B + * => A, D, E, F + * + * Under the old behavior the updated local state would instead have been + * => A, B, D, F + * + * Similarly, if a belongsTo remote State was A while its local state was B, + * then under the old behavior if the remote state changed to C, the local state + * would be updated to C. Under the new behavior, the local state would remain B. + * + * If the remote state was A while its local state was `null`, then under the old + * behavior if the remote state changed to C, the local state would be updated to C. + * Under the new behavior, the local state would remain `null`. + * + * Thus the new correct mental model is that the state of the relationship at any point + * in time is whatever the most recent remote state is, plus any local additions or removals + * you have made that have not yet been reflected by the remote state. + * + * > Note: The old behavior extended to modifying the inverse of a relationship. So if + * > you had local state not reflected in the new remote state, inverses would be notified + * > and their state reverted as well when "resetting" the relationship. + * > Under the new behavior, since the local state is preserved the inverses will also + * > not be reverted. + * + * ### Resolving this deprecation + * + * Resolving this deprecation can be done individually for each relationship + * or globally for all relationships. + * + * To resolve it globally, set the `DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE` + * to `false` in ember-cli-build.js + * + * ```js + * const { setConfig } = await import('@warp-drive/build-config'); + * + * let app = new EmberApp(defaults, {}); + * + * setConfig(app, __dirname, { + * deprecations: { + * // set to false to strip the deprecated code (thereby opting into the new behavior) + * DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE: false + * } + * }); + * ``` + * + * To resolve this deprecation on an individual relationship, adjust the `options` passed to + * the relationship. For relationships with inverses, both sides MUST be migrated to the new + * behavior at the same time. + * + * ```js + * class Person extends Model { + * @hasMany('person', { + * async: false, + * inverse: null, + * resetOnRemoteUpdate: false + * }) children; + * + * @belongsTo('person', { + * async: false, + * inverse: null, + * resetOnRemoteUpdate: false + * }) parent; + * } + * ``` + * + * > Note: false is the only valid value here, all other values (including missing) + * > will be treated as true, where `true` is the legacy behavior that is now deprecated. + * + * Once you have migrated all relationships, you can remove the the resetOnRemoteUpdate + * option and set the deprecation flag to false in ember-cli-build. + * + * ### What if I don't want the new behavior? + * + * EmberData's philosophy is to not make assumptions about your application. Where possible + * we seek out "100%" solutions – solutions that work for all use cases - and where that is + * not possible we default to "90%" solutions – solutions that work for the vast majority of use + * cases. In the case of "90%" solutions we look for primitives that allow you to resolve the + * 10% case in your application. If no such primitives exist, we provide an escape hatch that + * ensures you can build the behavior you need without adopting the cost of the default solution. + * + * In this case, the old behavior was a "40%" solution. The inability for an application developer + * to determine what changes were made locally, and thus what changes should be preserved, made + * it impossible to build certain features easily, or in some cases at all. The proliferation of + * feature requests, bug reports (from folks surprised by the prior behavior) and addon attempts + * in this space are all evidence of this. + * + * We believe the new behavior is a "90%" solution. It works for the vast majority of use cases, + * often without noticeable changes to existing application behavior, and provides primitives that + * allow you to build the behavior you need for the remaining 10%. + * + * The great news is that this behavior defaults to trusting your API similar to the old behavior. + * If your API is correct, you will not need to make any changes to your application to adopt + * the new behavior. + * + * This means the 10% cases are those where you can't trust your API to provide the correct + * information. In these cases, because you now have cheap access to a diff of the relationship + * state, there are a few options that weren't available before: + * + * - you can adjust returned API payloads to contain the expected changes that it doesn't include + * - you can modify local state by adding or removing records on the HasMany record array to remove + * any local changes that were not returned by the API. + * - you can use `.mutate(mutation)` to directly modify the local cache state of the relationship + * to match the expected state. + * + * What this version (5.3) does not yet provide is a way to directly modify the cache's remote state + * for the relationship via public APIs other than via the broader action of upserting a response via + * `.put(document)`. However, such an API was sketched in the Cache 2.1 RFC + * `.patch(operation)` and is likely to be added in a future 5.x release of EmberData. + * + * This version (5.3) also does not yet provide a way to directly modify the graph (a general purpose + * subset of cache behaviors specific to relationships) via public APIs. However, during the + * 5.x release series we will be working on finalizing the Graph API and making it public. + * + * If none of these options work for you, you can always opt-out more broadly by implementing + * a custom Cache with the relationship behaviors you need. + * + * @property DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE = '5.3'; + +/** + * **id: ember-data:deprecate-store-extends-ember-object** + * + * When the flag is `true` (default), the Store class will extend from `@ember/object`. + * When the flag is `false` or `ember-source` is not present, the Store will not extend + * from EmberObject. + * + * @property DEPRECATE_STORE_EXTENDS_EMBER_OBJECT + * @since 5.4 + * @until 6.0 + * @public + */ +export const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT = '5.4'; + +/** + * **id: ember-data:schema-service-updates** + * + * When the flag is `true` (default), the legacy schema + * service features will be enabled on the store and + * the service, and deprecations will be thrown when + * they are used. + * + * Deprecated features include: + * + * - `Store.registerSchema` method is deprecated in favor of the `Store.createSchemaService` hook + * - `Store.registerSchemaDefinitionService` method is deprecated in favor of the `Store.createSchemaService` hook + * - `Store.getSchemaDefinitionService` method is deprecated in favor of `Store.schema` property + * - `SchemaService.doesTypeExist` method is deprecated in favor of the `SchemaService.hasResource` method + * - `SchemaService.attributesDefinitionFor` method is deprecated in favor of the `SchemaService.fields` method + * - `SchemaService.relationshipsDefinitionFor` method is deprecated in favor of the `SchemaService.fields` method + * + * @property ENABLE_LEGACY_SCHEMA_SERVICE + * @since 5.4 + * @until 6.0 + * @public + */ +export const ENABLE_LEGACY_SCHEMA_SERVICE = '5.4'; + +/** + * **id: warp-drive.ember-inflector** + * + * Deprecates the use of ember-inflector for pluralization and singularization in favor + * of the `@ember-data/request-utils` package. + * + * Rule configuration methods (singular, plural, uncountable, irregular) and + * usage methods (singularize, pluralize) are are available as imports from + * `@ember-data/request-utils/string` + * + * Notable differences with ember-inflector: + * - there cannot be multiple inflector instances with separate rules + * - pluralization does not support a count argument + * - string caches now default to 10k entries instead of 1k, and this + * size is now configurable. Additionally, the cache is now a LRU cache + * instead of a first-N cache. + * + * This deprecation can be resolved by removing usage of ember-inflector or by using + * both ember-inflector and @ember-data/request-utils in parallel and updating your + * EmberData/WarpDrive build config to mark the deprecation as resolved + * in ember-cli-build + * + * ```js + * setConfig(app, __dirname, { deprecations: { DEPRECATE_EMBER_INFLECTOR: false }}); + * ``` + * + * @property DEPRECATE_EMBER_INFLECTOR + * @since 5.3 + * @until 6.0 + * @public + */ +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 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 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 5.x branch will + * print and can be resolved. + * + * @property DISABLE_6X_DEPRECATIONS + * @since 4.13 + * @until 5.0 + * @public + */ +export const DISABLE_6X_DEPRECATIONS = '6.0'; diff --git a/packages/build-config/src/deprecations.ts b/packages/build-config/src/deprecations.ts new file mode 100644 index 00000000000..344315da32f --- /dev/null +++ b/packages/build-config/src/deprecations.ts @@ -0,0 +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_NON_UNIQUE_PAYLOADS: boolean = true; +export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE: 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_6X_DEPRECATIONS: boolean = true; diff --git a/packages/build-config/src/env.ts b/packages/build-config/src/env.ts new file mode 100644 index 00000000000..7d29e25a9a3 --- /dev/null +++ b/packages/build-config/src/env.ts @@ -0,0 +1,6 @@ +export const DEBUG: boolean = true; +export const PRODUCTION: boolean = true; +export const TESTING: boolean = true; +export const IS_RECORDING: boolean = true; +export const IS_CI: boolean = true; +export const SHOULD_RECORD: boolean = true; diff --git a/packages/build-config/src/index.ts b/packages/build-config/src/index.ts new file mode 100644 index 00000000000..2b12add9fab --- /dev/null +++ b/packages/build-config/src/index.ts @@ -0,0 +1,97 @@ +import EmbroiderMacros from '@embroider/macros/src/node.js'; +import { getEnv } from './-private/utils/get-env.ts'; +import { getDeprecations } from './-private/utils/deprecations.ts'; +import { getFeatures } from './-private/utils/features.ts'; +import * as LOGGING from './debugging.ts'; +import type { MacrosConfig } from '@embroider/macros/src/node.js'; + +const _MacrosConfig = EmbroiderMacros.MacrosConfig as unknown as typeof MacrosConfig; + +type LOG_CONFIG_KEY = keyof typeof LOGGING; + +export type WarpDriveConfig = { + debug?: Partial; + polyfillUUID?: boolean; + includeDataAdapterInProduction?: boolean; + compatWith?: `${number}.${number}`; + deprecations?: Partial; + features?: Partial; +}; + +type InternalWarpDriveConfig = { + debug: { [key in LOG_CONFIG_KEY]: boolean }; + polyfillUUID: boolean; + includeDataAdapter: boolean; + compatWith: `${number}.${number}` | null; + deprecations: ReturnType; + features: ReturnType; + env: { + TESTING: boolean; + PRODUCTION: boolean; + DEBUG: boolean; + }; +}; + +type MacrosWithGlobalConfig = Omit & { globalConfig: Record }; + +function recastMacrosConfig(macros: object): MacrosWithGlobalConfig { + if (!('globalConfig' in macros)) { + throw new Error('Expected MacrosConfig to have a globalConfig property'); + } + return macros as MacrosWithGlobalConfig; +} + +export function setConfig(context: object, appRoot: string, config: WarpDriveConfig) { + const macros = recastMacrosConfig(_MacrosConfig.for(context, appRoot)); + const isLegacySupport = (config as unknown as { ___legacy_support?: boolean }).___legacy_support; + const hasDeprecatedConfig = isLegacySupport && Object.keys(config).length > 1; + const hasInitiatedConfig = macros.globalConfig['WarpDrive']; + + // setConfig called by user prior to legacy support called + if (isLegacySupport && hasInitiatedConfig) { + if (hasDeprecatedConfig) { + throw new Error( + 'You have provided a config object to setConfig, but are also using the legacy emberData options key in ember-cli-build. Please remove the emberData key from options.' + ); + } + return; + } + + // legacy support called prior to user setConfig + if (isLegacySupport && hasDeprecatedConfig) { + console.warn( + `You are using the legacy emberData key in your ember-cli-build.js file. This key is deprecated and will be removed in the next major version of EmberData/WarpDrive. Please use \`import { setConfig } from '@warp-drive/build-config';\` instead.` + ); + } + + // included hooks run during class initialization of the EmberApp instance + // so our hook will run before the user has a chance to call setConfig + // else we could print a useful message here + // else if (isLegacySupport) { + // console.warn( + // `WarpDrive requires your ember-cli-build file to set a base configuration for the project.\n\nUsage:\n\t\`import { setConfig } from '@warp-drive/build-config';\n\tsetConfig(app, __dirname, {});\`` + // ); + // } + + const debugOptions: InternalWarpDriveConfig['debug'] = Object.assign({}, LOGGING, config.debug); + + const env = getEnv(); + const DEPRECATIONS = getDeprecations(config.compatWith || null, config.deprecations); + const FEATURES = getFeatures(env.PRODUCTION); + + const includeDataAdapterInProduction = + typeof config.includeDataAdapterInProduction === 'boolean' ? config.includeDataAdapterInProduction : true; + const includeDataAdapter = env.PRODUCTION ? includeDataAdapterInProduction : true; + + const finalizedConfig: InternalWarpDriveConfig = { + debug: debugOptions, + polyfillUUID: config.polyfillUUID ?? false, + includeDataAdapter, + compatWith: config.compatWith ?? null, + deprecations: DEPRECATIONS, + features: FEATURES, + env, + }; + + macros.setGlobalConfig(import.meta.filename, 'WarpDrive', finalizedConfig); +} diff --git a/packages/build-config/src/macros.ts b/packages/build-config/src/macros.ts new file mode 100644 index 00000000000..d69efe9115a --- /dev/null +++ b/packages/build-config/src/macros.ts @@ -0,0 +1,7 @@ +export function assert(message: string, condition: unknown): asserts condition; +export function assert(message: string): never; +export function assert(message: string, condition?: unknown): asserts condition { + if (!condition) { + throw new Error(message); + } +} diff --git a/packages/build-config/src/validate-exports.type-test.ts b/packages/build-config/src/validate-exports.type-test.ts new file mode 100644 index 00000000000..6bcb452f3dd --- /dev/null +++ b/packages/build-config/src/validate-exports.type-test.ts @@ -0,0 +1,14 @@ +import * as DEPRECATION_VERSIONS from './deprecation-versions'; +import * as DEPRECATION_FLAGS from './deprecations'; + +function expectKeyMatch, K extends Record>( + actual: T, + expected: K +): void {} + +// If this is failing, it means that the exported deprecation flags in +// ./deprecations.ts are out of sync with the version flags in +// ./deprecation-versions.ts. This is likely because a new deprecation flag was +// added or removed without updating the other file. +expectKeyMatch(DEPRECATION_VERSIONS, DEPRECATION_FLAGS); +expectKeyMatch(DEPRECATION_FLAGS, DEPRECATION_VERSIONS); diff --git a/packages/build-config/tsconfig.json b/packages/build-config/tsconfig.json new file mode 100644 index 00000000000..62c40e41edd --- /dev/null +++ b/packages/build-config/tsconfig.json @@ -0,0 +1,45 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "alwaysStrict": true, + "downlevelIteration": false, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": false, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitAny": true, + "noImplicitOverride": false, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true + }, + "references": [] +} diff --git a/packages/build-config/vite.config-cjs.mjs b/packages/build-config/vite.config-cjs.mjs new file mode 100644 index 00000000000..c55ece3a903 --- /dev/null +++ b/packages/build-config/vite.config-cjs.mjs @@ -0,0 +1,26 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = ['babel-import-util', 'fs', 'path', 'url']; + +export const entryPoints = [ + './cjs-src/transforms/babel-plugin-transform-asserts.js', + './cjs-src/transforms/babel-plugin-transform-deprecations.js', + './cjs-src/transforms/babel-plugin-transform-features.js', + './cjs-src/transforms/babel-plugin-transform-logging.js', + './cjs-src/addon-shim.js', + './src/cjs-set-config.ts', +]; + +export default createConfig( + { + entryPoints, + flatten: true, + format: 'cjs', + externals, + target: ['esnext', 'firefox121', 'node18'], + emptyOutDir: false, + fixModule: false, + compileTypes: false, + }, + import.meta.resolve +); diff --git a/packages/build-config/vite.config.mjs b/packages/build-config/vite.config.mjs new file mode 100644 index 00000000000..78033fcc611 --- /dev/null +++ b/packages/build-config/vite.config.mjs @@ -0,0 +1,23 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = ['fs', 'path', 'semver', 'url']; + +export const entryPoints = [ + './src/index.ts', + './src/babel-macros.ts', + './src/env.ts', + './src/macros.ts', + './src/debugging.ts', + './src/deprecations.ts', + './src/canary-features.ts', +]; + +export default createConfig( + { + entryPoints, + flatten: true, + externals, + fixModule: false, + }, + import.meta.resolve +); diff --git a/packages/core-types/CHANGELOG.md b/packages/core-types/CHANGELOG.md new file mode 100644 index 00000000000..2d583229c7d --- /dev/null +++ b/packages/core-types/CHANGELOG.md @@ -0,0 +1,73 @@ +# @warp-drive/core-types Changelog + +## v0.0.0-alpha.71 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@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)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#9314](https://github.com/emberjs/data/pull/9314) feat: improve lifetime handling of ad-hoc createRecord requests ([@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)) +* [#9245](https://github.com/emberjs/data/pull/9245) feat: add consumer types for Model APIs ([@runspired](https://github.com/runspired)) +* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9476](https://github.com/emberjs/data/pull/9476) chore: cleanup symbol usage ([@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)) +* [#9279](https://github.com/emberjs/data/pull/9279) types: branded transforms and improve types needed for serializers ([@runspired](https://github.com/runspired)) + +#### Committers: (3) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Rich Glazerman ([@richgt](https://github.com/richgt)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v0.0.0-alpha.9 (2024-02-24) + +#### :memo: Documentation + +* [#9161](https://github.com/emberjs/data/pull/9161) docs: fix return signature of peekRequest ([@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)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) +* [#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)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/core-types/LICENSE.md b/packages/core-types/LICENSE.md new file mode 100644 index 00000000000..ee1ae5bf425 --- /dev/null +++ b/packages/core-types/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (C) 2023 EmberData and WarpDrive contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/core-types/NCC-1701-a-blue.svg b/packages/core-types/NCC-1701-a-blue.svg new file mode 100644 index 00000000000..3b46f232c1a --- /dev/null +++ b/packages/core-types/NCC-1701-a-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core-types/NCC-1701-a.svg b/packages/core-types/NCC-1701-a.svg new file mode 100644 index 00000000000..8ee688dcf30 --- /dev/null +++ b/packages/core-types/NCC-1701-a.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core-types/README.md b/packages/core-types/README.md new file mode 100644 index 00000000000..9fa9399ffad --- /dev/null +++ b/packages/core-types/README.md @@ -0,0 +1,92 @@ +

+ + +

+ +

🛸 @warp-drive/core-types

+

Provides core types, type utils and constants for WarpDrive and EmberData

+ +## Installation + +```cli +pnpm install @warp-drive/core-types +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/core-types/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40warp-drive/core-types/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40warp-drive/core-types/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40warp-drive/core-types/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40warp-drive/core-types/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + +### ♥️ Credits + +
+ Brought to you with ♥️ love by 🐹 Ember + + +
diff --git a/packages/core-types/addon-main.cjs b/packages/core-types/addon-main.cjs new file mode 100644 index 00000000000..aa712cb9b1e --- /dev/null +++ b/packages/core-types/addon-main.cjs @@ -0,0 +1,4 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); +module.exports = addonShim(__dirname); diff --git a/packages/core-types/babel.config.mjs b/packages/core-types/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/core-types/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/core-types/eslint.config.mjs b/packages/core-types/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/core-types/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/core-types/package.json b/packages/core-types/package.json new file mode 100644 index 00000000000..2d8a57b25c1 --- /dev/null +++ b/packages/core-types/package.json @@ -0,0 +1,73 @@ +{ + "name": "@warp-drive/core-types", + "version": "4.12.8", + "description": "Provides core logic, utils and types for WarpDrive and EmberData", + "keywords": [ + "ember-addon" + ], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/core-types" + }, + "license": "MIT", + "author": "Chris Thoburn ", + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "files": [ + "dist", + "addon-main.cjs", + "unstable-preview-types", + "README.md", + "LICENSE.md", + "NCC-1701-a.svg", + "NCC-1701-a-blue.svg" + ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@warp-drive/internal-config": "workspace:*", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 + }, + "ember": { + "edition": "octane" + }, + "dependenciesMeta": { + "@warp-drive/build-config": { + "injected": true + } + } +} diff --git a/packages/core-types/src/-private.ts b/packages/core-types/src/-private.ts new file mode 100644 index 00000000000..a08ec018525 --- /dev/null +++ b/packages/core-types/src/-private.ts @@ -0,0 +1,195 @@ +// in testing mode, we utilize globals to ensure only one copy exists of +// these maps, due to bugs in ember-auto-import +import { DEBUG, TESTING } from '@warp-drive/build-config/env'; + +import { name, version } from '../package.json'; + +type UniversalTransientKey = + // @ember-data/request + 'REQ_ID'; + +type UniversalKey = + | `(transient) ${UniversalTransientKey}` + // @ember-data/request + | 'RequestMap' + | 'PromiseCache' + | 'RequestCache' + // @warp-drive/core-types/request + | 'SkipCache' + | 'EnableHydration'; + +type TransientKey = + // @ember-data/tracking + | 'TRANSACTION' + // @ember-data/graph + | 'transactionRef' + // @ember-data/store + | 'configuredGenerationMethod' + | 'configuredUpdateMethod' + | 'configuredForgetMethod' + | 'configuredResetMethod' + | 'configuredKeyInfoMethod'; + +type GlobalKey = + | `(transient) ${TransientKey}` + // @ember-data/adapter + | 'AdapterError' + | 'InvalidError' + | 'TimeoutError' + | 'AbortError' + | 'UnauthorizedError' + | 'ForbiddenError' + | 'NotFoundError' + | 'ConflictError' + | 'ServerError' + // @ember-data/tracking + | 'Signals' + // @ember-data/store LegacySupport + | 'AvailableShims' + // @ember-data/store RecordArrayManager + | 'FAKE_ARR' + // @ember-data/store IdentifierArray + | '#signal' + | '#source' + | '#update' + | '#notify' + | 'IS_COLLECTION' + // @ember-data/store RequestCache + | 'Touching' + | 'RequestPromise' + // @ember-data/legacy-compat FetchManager + | 'SaveOp' + // @ember-data/model + | 'LEGACY_SUPPORT' + | 'LegacySupport' + // @ember-data/graph + | 'Graphs' + // @ember-data/request + | 'IS_FROZEN' + | 'IS_CACHE_HANDLER' + // @ember-data/store IdentityCache + | 'DEBUG_MAP' + | 'IDENTIFIERS' + | 'DOCUMENTS' + // @ember-data/store InstanceCache + | 'CacheForIdentifierCache' + | 'RecordCache' + | 'StoreMap' + // @warp-drive/core-types/symbols + | 'Store' + | '$type' + | 'TransformName' + | 'RequestSignature' + // @warp-drive/core-types/request + | 'IS_FUTURE' + | 'DOC' + // @warp-drive/schema-record + | 'ManagedArrayMap' + | 'ManagedObjectMap' + | 'Support' + | 'SOURCE' + | 'MUTATE' + | 'ARRAY_SIGNAL' + | 'OBJECT_SIGNAL' + | 'NOTIFY' + | 'Destroy' + | 'Identifier' + | 'Editable' + | 'EmbeddedPath' + | 'EmbeddedType' + | 'Parent' + | 'Checkout' + | 'Legacy'; + +type ModuleScopedCaches = Record; + +const GlobalRef = globalThis as unknown as Record< + string, + { + __warpDrive_ModuleScopedCaches?: ModuleScopedCaches; + __warpDrive_hasOtherCopy?: boolean; + __version: string; + } +> & { + __warpDrive_universalCache: Record; +}; +const UniversalCache = (GlobalRef.__warpDrive_universalCache = + GlobalRef.__warpDrive_universalCache ?? ({} as Record)); + +// in order to support mirror packages, we ensure that each +// unique package name has its own global cache +GlobalRef[name] = GlobalRef[name] ?? { __version: version }; +const GlobalSink = GlobalRef[name]; + +if (DEBUG) { + if (GlobalSink.__version !== version) { + throw new Error('Multiple versions of WarpDrive detected, the application will malfunction.'); + } +} + +const ModuleScopedCaches = GlobalSink.__warpDrive_ModuleScopedCaches ?? ({} as ModuleScopedCaches); +if (TESTING) { + if (!GlobalSink.__warpDrive_ModuleScopedCaches) { + GlobalSink.__warpDrive_ModuleScopedCaches = ModuleScopedCaches; + } else { + // eslint-disable-next-line no-console + console.warn(` +Multiple copies of EmberData have been detected. This may be due to a bug in ember-auto-import + in which test assets get their own copy of some v2-addons. This can cause the application to + malfunction as each copy will maintain its own separate state.`); + } +} else { + if (GlobalSink.__warpDrive_hasOtherCopy) { + throw new Error('Multiple copies of EmberData detected, the application will malfunction.'); + } + GlobalSink.__warpDrive_hasOtherCopy = true; +} + +type UniqueSymbol = `___(unique) Symbol(${T})`; +type UniqueSymbolOr = T extends symbol ? UniqueSymbol : T; + +export function getOrSetGlobal(key: K, value: T): UniqueSymbolOr { + if (TESTING) { + const existing = ModuleScopedCaches[key]; + if (existing === undefined) { + return (ModuleScopedCaches[key] = value) as UniqueSymbolOr; + } else { + return existing as UniqueSymbolOr; + } + } else { + return value as UniqueSymbolOr; + } +} + +export function peekTransient(key: TransientKey): T | null { + const globalKey: `(transient) ${TransientKey}` = `(transient) ${key}`; + return (ModuleScopedCaches[globalKey] as T) ?? null; +} + +export function setTransient(key: TransientKey, value: T): T { + const globalKey: `(transient) ${TransientKey}` = `(transient) ${key}`; + return (ModuleScopedCaches[globalKey] = value); +} + +export function getOrSetUniversal(key: K, value: T): UniqueSymbolOr { + if (TESTING) { + const existing = UniversalCache[key]; + if (existing === undefined) { + return (UniversalCache[key] = value) as UniqueSymbolOr; + } else { + return existing as UniqueSymbolOr; + } + } else { + return value as UniqueSymbolOr; + } +} + +export function peekUniversalTransient(key: UniversalTransientKey): T | null { + const globalKey: `(transient) ${UniversalTransientKey}` = `(transient) ${key}`; + return (UniversalCache[globalKey] as T) ?? null; +} + +export function setUniversalTransient(key: UniversalTransientKey, value: T): T { + const globalKey: `(transient) ${UniversalTransientKey}` = `(transient) ${key}`; + return (UniversalCache[globalKey] = value); +} diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts new file mode 100644 index 00000000000..a219c3b4f81 --- /dev/null +++ b/packages/core-types/src/cache.ts @@ -0,0 +1,531 @@ +/** + * @module @ember-data/experimental-preview-types + */ +import type { ResourceBlob } from './cache/aliases'; +import type { Change } from './cache/change'; +import type { Mutation } from './cache/mutations'; +import type { Operation } from './cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from './cache/relationship'; +import type { StableDocumentIdentifier, StableRecordIdentifier } from './identifier'; +import type { Value } from './json/raw'; +import type { TypeFromInstanceOrString } from './record'; +import type { RequestContext, StructuredDataDocument, StructuredDocument } from './request'; +import type { ResourceDocument, SingleResourceDataDocument } from './spec/document'; +import type { ApiError } from './spec/error'; + +/** + * A hash of changed attributes with the key being the attribute name and the value being an + * array of `[oldValue, newValue]`. + * + * @internal + */ +export type ChangedAttributesHash = Record; + +export type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; + } + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + +/** + * The interface for EmberData Caches. + * + * A Cache handles in-memory storage of Document and Resource + * data. + * + * @class Cache + * @public + */ +export interface Cache { + /** + * The Cache Version that this implementation implements. + * + * @type {'2'} + * @public + * @property version + */ + version: '2'; + + // Cache Management + // ================ + + /** + * Cache the response to a request + * + * Unlike `store.push` which has UPSERT + * semantics, `put` has `replace` semantics similar to + * the `http` method `PUT` + * + * the individually cacheable resource data it may contain + * should upsert, but the document data surrounding it should + * fully replace any existing information + * + * Note that in order to support inserting arbitrary data + * to the cache that did not originate from a request `put` + * should expect to sometimes encounter a document with only + * a `content` member and therefor must not assume the existence + * of `request` and `response` on the document. + * + * @method put + * @param {StructuredDocument} doc + * @return {ResourceDocument} + * @public + */ + put(doc: StructuredDocument | { content: T }): ResourceDocument; + + /** + * Update the "remote" or "canonical" (persisted) state of the Cache + * by merging new information into the existing state. + * + * Note: currently the only valid resource operation is a MergeOperation + * which occurs when a collision of identifiers is detected. + * + * @method patch + * @public + * @param {Operation} op the operation to perform + * @return {void} + */ + patch(op: Operation): void; + + /** + * Update the "local" or "current" (unpersisted) state of the Cache + * + * @method mutate + * @param {Mutation} mutation + * @return {void} + * @public + */ + mutate(mutation: Mutation): void; + + /** + * Peek resource data from the Cache. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @public + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @return {ResourceDocument | ResourceBlob | null} the known resource data + */ + peek(identifier: StableRecordIdentifier>): T | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + + /** + * Peek the Cache for the existing request data associated with + * a cacheable request + * + * This is effectively the reverse of `put` for a request in + * that it will return the the request, response, and content + * whereas `peek` will return just the `content`. + * + * @method peekRequest + * @param {StableDocumentIdentifier} + * @return {StructuredDocument | null} + * @public + */ + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + + /** + * Push resource data from a remote source into the cache for this identifier + * + * @method upsert + * @public + * @param identifier + * @param data + * @param hasRecord + * @return {void | string[]} if `hasRecord` is true then calculated key changes should be returned + */ + upsert(identifier: StableRecordIdentifier, data: ResourceBlob, hasRecord: boolean): void | string[]; + + // Cache Forking Support + // ===================== + + /** + * Create a fork of the cache from the current state. + * + * Applications should typically not call this method themselves, + * preferring instead to fork at the Store level, which will + * utilize this method to fork the cache. + * + * @method fork + * @public + * @return Promise + */ + fork(): Promise; + + /** + * Merge a fork back into a parent Cache. + * + * Applications should typically not call this method themselves, + * preferring instead to merge at the Store level, which will + * utilize this method to merge the caches. + * + * @method merge + * @param {Cache} cache + * @public + * @return Promise + */ + merge(cache: Cache): Promise; + + /** + * Generate the list of changes applied to all + * record in the store. + * + * Each individual resource or document that has + * been mutated should be described as an individual + * `Change` entry in the returned array. + * + * A `Change` is described by an object containing up to + * three properties: (1) the `identifier` of the entity that + * changed; (2) the `op` code of that change being one of + * `upsert` or `remove`, and if the op is `upsert` a `patch` + * containing the data to merge into the cache for the given + * entity. + * + * This `patch` is opaque to the Store but should be understood + * by the Cache and may expect to be utilized by an Adapter + * when generating data during a `save` operation. + * + * It is generally recommended that the `patch` contain only + * the updated state, ignoring fields that are unchanged + * + * ```ts + * interface Change { + * identifier: StableRecordIdentifier | StableDocumentIdentifier; + * op: 'upsert' | 'remove'; + * patch?: unknown; + * } + * ``` + * + * @method diff + * @public + */ + diff(): Promise; + + // SSR Support + // =========== + + /** + * Serialize the entire contents of the Cache into a Stream + * which may be fed back into a new instance of the same Cache + * via `cache.hydrate`. + * + * @method dump + * @return {Promise} + * @public + */ + dump(): Promise>; + + /** + * hydrate a Cache from a Stream with content previously serialized + * from another instance of the same Cache, resolving when hydration + * is complete. + * + * This method should expect to be called both in the context of restoring + * the Cache during application rehydration after SSR **AND** at unknown + * times during the lifetime of an already booted application when it is + * desired to bulk-load additional information into the cache. This latter + * behavior supports optimizing pre/fetching of data for route transitions + * via data-only SSR modes. + * + * @method hydrate + * @param {ReadableStream} stream + * @return {Promise} + * @public + */ + hydrate(stream: ReadableStream): Promise; + + // Resource Support + // ================ + + /** + * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client + * + * It returns properties from options that should be set on the record during the create + * process. This return value behavior is deprecated. + * + * @method clientDidCreate + * @public + * @param identifier + * @param createArgs + */ + clientDidCreate(identifier: StableRecordIdentifier, createArgs?: Record): Record; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * will be part of a save transaction. + * + * @method willCommit + * @public + * @param identifier + */ + willCommit(identifier: StableRecordIdentifier, context: RequestContext): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was successfully updated as part of a save transaction. + * + * @method didCommit + * @public + * @param identifier - the primary identifier that was operated on + * @param data - a document in the cache format containing any updated data + * @return {SingleResourceDataDocument} + */ + didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was update via a save transaction failed. + * + * @method commitWasRejected + * @public + * @param identifier + * @param errors + */ + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void; + + /** + * [LIFECYCLE] Signals to the cache that all data for a resource + * should be cleared. + * + * This method is a candidate to become a mutation + * + * @method unloadRecord + * @public + * @param identifier + */ + unloadRecord(identifier: StableRecordIdentifier): void; + + // Granular Resource Data APIs + // =========================== + + /** + * Retrieve the data for an attribute from the cache + * + * @method getAttr + * @public + * @param identifier + * @param field + * @return {unknown} + */ + getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + + /** + * Mutate the data for an attribute in the cache + * + * This method is a candidate to become a mutation + * + * @method setAttr + * @public + * @param identifier + * @param field + * @param value + */ + setAttr(identifier: StableRecordIdentifier, field: string | string[], value: Value): void; + + /** + * Query the cache for the changed attributes of a resource. + * + * Returns a map of field names to tuples of [old, new] values + * + * ``` + * { : [, ] } + * ``` + * + * @method changedAttrs + * @public + * @param identifier + * @return {Record} { : [, ] } + */ + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedAttrs + * @public + * @param identifier + * @return {boolean} + */ + hasChangedAttrs(identifier: StableRecordIdentifier): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to attributes + * + * This method is a candidate to become a mutation + * + * @method rollbackAttrs + * @public + * @param identifier + * @return {string[]} the names of fields that were restored + */ + rollbackAttrs(identifier: StableRecordIdentifier): string[]; + + /** + * Query the cache for the changes to relationships of a resource. + * + * Returns a map of relationship names to RelationshipDiff objects. + * + * ```ts + * type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; + } + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + ``` + * + * @method changedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {Map} + */ + changedRelationships(identifier: StableRecordIdentifier): Map; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {boolean} + */ + hasChangedRelationships(identifier: StableRecordIdentifier): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to relationships. + * + * This will also discard the change on any appropriate inverses. + * + * This method is a candidate to become a mutation + * + * @method rollbackRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {string[]} the names of relationships that were restored + */ + rollbackRelationships(identifier: StableRecordIdentifier): string[]; + + /** + * Query the cache for the current state of a relationship property + * + * @method getRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return resource relationship object + */ + getRelationship( + identifier: StableRecordIdentifier, + field: string, + isCollection?: boolean + ): ResourceRelationship | CollectionRelationship; + + // Resource State + // =============== + + /** + * Update the cache state for the given resource to be marked + * as locally deleted, or remove such a mark. + * + * This method is a candidate to become a mutation + * + * @method setIsDeleted + * @public + * @param identifier + * @param isDeleted {boolean} + */ + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void; + + /** + * Query the cache for any validation errors applicable to the given resource. + * + * @method getErrors + * @public + * @param identifier + * @return {JsonApiError[]} + */ + getErrors(identifier: StableRecordIdentifier): ApiError[]; + + /** + * Query the cache for whether a given resource has any available data + * + * @method isEmpty + * @public + * @param identifier + * @return {boolean} + */ + isEmpty(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource was created locally and not + * yet persisted. + * + * @method isNew + * @public + * @param identifier + * @return {boolean} + */ + isNew(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource is marked as deleted (but not + * necessarily persisted yet). + * + * @method isDeleted + * @public + * @param identifier + * @return {boolean} + */ + isDeleted(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource has been deleted and that deletion + * has also been persisted. + * + * @method isDeletionCommitted + * @public + * @param identifier + * @return {boolean} + */ + isDeletionCommitted(identifier: StableRecordIdentifier): boolean; +} diff --git a/ember-data-types/cache/aliases.ts b/packages/core-types/src/cache/aliases.ts similarity index 100% rename from ember-data-types/cache/aliases.ts rename to packages/core-types/src/cache/aliases.ts diff --git a/packages/core-types/src/cache/change.ts b/packages/core-types/src/cache/change.ts new file mode 100644 index 00000000000..07a821d0037 --- /dev/null +++ b/packages/core-types/src/cache/change.ts @@ -0,0 +1,7 @@ +import type { StableDocumentIdentifier, StableRecordIdentifier } from '../identifier'; + +export interface Change { + identifier: StableRecordIdentifier | StableDocumentIdentifier; + op: 'upsert' | 'remove'; + patch?: unknown; +} diff --git a/ember-data-types/cache/mutations.ts b/packages/core-types/src/cache/mutations.ts similarity index 96% rename from ember-data-types/cache/mutations.ts rename to packages/core-types/src/cache/mutations.ts index 684a0e868ab..e7ff59bcf60 100644 --- a/ember-data-types/cache/mutations.ts +++ b/packages/core-types/src/cache/mutations.ts @@ -1,4 +1,4 @@ -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableRecordIdentifier } from '../identifier'; export interface AddToRelatedRecordsMutation { op: 'addToRelatedRecords'; diff --git a/ember-data-types/cache/operations.ts b/packages/core-types/src/cache/operations.ts similarity index 91% rename from ember-data-types/cache/operations.ts rename to packages/core-types/src/cache/operations.ts index 82c6c012e45..0a0d8dd280f 100644 --- a/ember-data-types/cache/operations.ts +++ b/packages/core-types/src/cache/operations.ts @@ -1,4 +1,4 @@ -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableRecordIdentifier } from '../identifier'; export interface Op { op: string; diff --git a/packages/core-types/src/cache/relationship.ts b/packages/core-types/src/cache/relationship.ts new file mode 100644 index 00000000000..82b117d7788 --- /dev/null +++ b/packages/core-types/src/cache/relationship.ts @@ -0,0 +1,19 @@ +import type { StableRecordIdentifier } from '../identifier'; +import type { Links, Meta, PaginationLinks } from '../spec/json-api-raw'; + +// we request that it be in the stable form already. +export interface ResourceRelationship { + data?: StableRecordIdentifier | null; + meta?: Meta; + links?: Links; +} + +// Note: in v1 data could be a ResourceIdentifier, now +// we request that it be in the stable form already. +export interface CollectionRelationship { + data?: StableRecordIdentifier[]; + meta?: Meta; + links?: PaginationLinks; +} + +export type Relationship = ResourceRelationship | CollectionRelationship; diff --git a/packages/core-types/src/graph.ts b/packages/core-types/src/graph.ts new file mode 100644 index 00000000000..89f6218d804 --- /dev/null +++ b/packages/core-types/src/graph.ts @@ -0,0 +1,91 @@ +import type { CollectionRelationship, ResourceRelationship } from './cache/relationship'; +import type { StableRecordIdentifier } from './identifier'; +import type { CollectionResourceRelationship, SingleResourceRelationship } from './spec/json-api-raw'; + +export interface Graph { + identifiers: Map; + + getData(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship; + + remove(identifier: StableRecordIdentifier): void; + registerPolymorphicType(abstract: string, concrete: string): void; + destroy(): void; +} + +export interface Operation { + op: string; +} + +export interface UpdateRelationshipOperation { + op: 'updateRelationship'; + record: StableRecordIdentifier; + field: string; + value: SingleResourceRelationship | CollectionResourceRelationship; +} + +export interface DeleteRecordOperation { + op: 'deleteRecord'; + record: StableRecordIdentifier; + isNew: boolean; +} + +export interface UnknownOperation { + op: 'never'; + record: StableRecordIdentifier; + field: string; +} + +export interface AddToRelatedRecordsOperation { + op: 'addToRelatedRecords'; + record: StableRecordIdentifier; + field: string; // "relationship" propertyName + value: StableRecordIdentifier | StableRecordIdentifier[]; // related record + index?: number; // the index to insert at +} + +export interface RemoveFromRelatedRecordsOperation { + op: 'removeFromRelatedRecords'; + record: StableRecordIdentifier; + field: string; // "relationship" propertyName + value: StableRecordIdentifier | StableRecordIdentifier[]; // related record + index?: number; // optional the index at which we're expected to start the removal +} + +export interface ReplaceRelatedRecordOperation { + op: 'replaceRelatedRecord'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier | null; // never null if field is a collection + prior?: StableRecordIdentifier; // if field is a collection, the value we are swapping with + index?: number; // if field is a collection, the index at which we are replacing a value +} + +export interface SortRelatedRecords { + op: 'sortRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier[]; +} + +export interface ReplaceRelatedRecordsOperation { + op: 'replaceRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier[]; // the records to add. If no prior/index specified all existing should be removed + prior?: StableRecordIdentifier[]; // if this is a "splice" the records we expect to be removed + index?: number; // if this is a "splice" the index to start from +} + +export type RemoteRelationshipOperation = + | UpdateRelationshipOperation + | ReplaceRelatedRecordOperation + | ReplaceRelatedRecordsOperation + | DeleteRecordOperation + | SortRelatedRecords; + +export type LocalRelationshipOperation = + | ReplaceRelatedRecordsOperation + | ReplaceRelatedRecordOperation + | RemoveFromRelatedRecordsOperation + | AddToRelatedRecordsOperation + | SortRelatedRecords; diff --git a/packages/core-types/src/identifier.ts b/packages/core-types/src/identifier.ts new file mode 100644 index 00000000000..605c5ef1d07 --- /dev/null +++ b/packages/core-types/src/identifier.ts @@ -0,0 +1,125 @@ +/** + @module @ember-data/store +*/ + +// 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'); + +// also present in production +export const CACHE_OWNER: unique symbol = Symbol('warpDriveCache'); + +export type IdentifierBucket = 'record' | 'document'; + +export interface Identifier { + lid: string; + clientId?: string; +} + +export interface ExistingRecordIdentifier extends Identifier { + id: string; + type: T; +} + +export interface NewRecordIdentifier extends Identifier { + id: string | null; + type: T; +} + +export type StableDocumentIdentifier = { + lid: string; +}; + +/** + * An Identifier specific to a record which may or may not + * be present in the cache. + * + * The absence of an `id` DOES NOT indicate that this + * Identifier is for a new client-created record as it + * may also indicate that it was generated for a secondary + * index and the primary `id` index is not yet known. + * + * @internal + */ +export type RecordIdentifier = ExistingRecordIdentifier | NewRecordIdentifier; + +/** + * Used when an Identifier is known to be the stable version + * + * @internal + */ +export interface StableIdentifier extends Identifier { + [DEBUG_IDENTIFIER_BUCKET]?: string; +} + +/** + * Used when a StableRecordIdentifier was not created locally as part + * of a call to store.createRecord + * + * Distinguishing between this Identifier and one for a client created + * record that was created with an ID is generally speaking not possible + * at runtime, so anything with an ID typically narrows to this. + * + * @internal + */ +export interface StableExistingRecordIdentifier extends StableIdentifier { + id: string; + type: T; + [DEBUG_CLIENT_ORIGINATED]?: boolean; + [CACHE_OWNER]: number | undefined; + [DEBUG_STALE_CACHE_OWNER]?: number | undefined; +} + +/** + * Used when a StableRecordIdentifier was created locally + * (by a call to store.createRecord). + * + * It is possible in rare circumstances to have a StableRecordIdentifier + * that is not for a new record but does not have an ID. This would + * happen if a user intentionally created one for use with a secondary-index + * prior to the record having been fully loaded. + * + * @internal + */ +export interface StableNewRecordIdentifier extends StableIdentifier { + id: string | null; + type: T; + [DEBUG_CLIENT_ORIGINATED]?: boolean; + [CACHE_OWNER]: number | undefined; + [DEBUG_STALE_CACHE_OWNER]?: number | undefined; +} + +/** + * A referentially stable object with a unique string (lid) that can be used + * as a reference to data in the cache. + * + * Every record instance has a unique identifier, and identifiers may refer + * to data that has never been loaded (for instance, in an async relationship). + * + * @class StableRecordIdentifier + * @public + */ + +/** + * A string representing a unique identity. + * + * @property {string} lid + * @public + */ +/** + * the primary resource `type` or `modelName` this identity belongs to. + * + * @property {string} type + * @public + */ +/** + * the primary id for the record this identity belongs to. `null` + * if not yet assigned an id. + * + * @property {string | null} id + * @public + */ +export type StableRecordIdentifier = + | StableExistingRecordIdentifier + | StableNewRecordIdentifier; diff --git a/packages/core-types/src/index.ts b/packages/core-types/src/index.ts new file mode 100644 index 00000000000..91ad7caba01 --- /dev/null +++ b/packages/core-types/src/index.ts @@ -0,0 +1 @@ +export type { StableRecordIdentifier } from './identifier'; diff --git a/packages/core-types/src/json/raw.ts b/packages/core-types/src/json/raw.ts new file mode 100644 index 00000000000..4f493dc8cf6 --- /dev/null +++ b/packages/core-types/src/json/raw.ts @@ -0,0 +1,7 @@ +export type PrimitiveValue = string | number | boolean | null; +export interface ObjectValue { + [key: string]: Value; +} +export type ArrayValue = Value[]; + +export type Value = PrimitiveValue | ArrayValue | ObjectValue; diff --git a/packages/core-types/src/params.ts b/packages/core-types/src/params.ts new file mode 100644 index 00000000000..418e9493cb1 --- /dev/null +++ b/packages/core-types/src/params.ts @@ -0,0 +1,14 @@ +import type { Includes, TypedRecordInstance } from './record'; + +export type SerializablePrimitive = string | number | boolean | null; +export type Serializable = SerializablePrimitive | SerializablePrimitive[]; +export type QueryParamsSerializationOptions = { + arrayFormat?: 'bracket' | 'indices' | 'repeat' | 'comma'; +}; + +export type QueryParamsSource = + | ({ include?: T extends TypedRecordInstance ? Includes[] : string | string[] } & Record< + Exclude, + Serializable + >) + | URLSearchParams; diff --git a/packages/core-types/src/record.ts b/packages/core-types/src/record.ts new file mode 100644 index 00000000000..9403b168746 --- /dev/null +++ b/packages/core-types/src/record.ts @@ -0,0 +1,200 @@ +/* + * @module @warp-drive/core-types + */ +import type { Type } from './symbols'; + +/** + * Records may be anything, They don't even + * have to be objects. + * + * Whatever they are, if they have a Type + * property, that property will be used by EmberData + * and WarpDrive to provide better type safety and + * intellisense. + * + * @class TypedRecordInstance + * @typedoc + */ +export interface TypedRecordInstance { + /** + * The type of the resource. + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * @property {Type} [Type] + * @type {string} + * @typedoc + */ + [Type]: string; +} + +/** + * A type utility that extracts the Type if available, + * otherwise it returns never. + * + * @typedoc + */ +export type TypeFromInstance = T extends TypedRecordInstance ? T[typeof Type] : never; + +/** + * A type utility that extracts the Type if available, + * otherwise it returns string + * + * @typedoc + */ +export type TypeFromInstanceOrString = T extends TypedRecordInstance ? T[typeof Type] : string; + +type IsUniqueSymbol = T extends `___(unique) Symbol(${string})` ? true : false; +type Unpacked = T extends (infer U)[] ? U : T; +type NONE = { __NONE: never }; + +type __InternalExtract< + MAX_DEPTH extends _DEPTHCOUNT, + T extends TypedRecordInstance, + V extends TypedRecordInstance, + IncludePrefix extends boolean, + Ignore, + Pre extends string, + DEPTH extends _DEPTHCOUNT, +> = + // if we extend T, we return the leaf value + V extends T + ? IncludePrefix extends false + ? V[typeof Type] + : Pre + : // else if we are in Ignore we add the lead and exit + V extends Ignore + ? IncludePrefix extends false + ? V[typeof Type] + : Pre + : // else if we are at max depth, we return never + IS_MAX_DEPTH extends true + ? Pre + : // else add T to Ignore and recurse + ExtractUnion>; + +type __ExtractIfRecord< + MAX_DEPTH extends _DEPTHCOUNT, + T extends TypedRecordInstance, + V, + IncludePrefix extends boolean, + Ignore, + Pre extends string, + DEPTH extends _DEPTHCOUNT, +> = V extends TypedRecordInstance ? __InternalExtract : never; + +type _ExtractUnion< + MAX_DEPTH extends _DEPTHCOUNT, + T extends TypedRecordInstance, + IncludePrefix extends boolean, + Ignore, + Pre, + DEPTH extends _DEPTHCOUNT, +> = { + // for each string key in the record, + [K in keyof T]: IsUniqueSymbol extends true + ? never + : K extends string + ? // we recursively extract any values that resolve to a TypedRecordInstance + __ExtractIfRecord< + MAX_DEPTH, + T, + Unpacked>, + IncludePrefix, + Ignore, + Pre extends string ? `${Pre}.${K}` : K, + DEPTH + > + : never; + // then we return any value that is not 'never' +}[keyof T]; + +/** + * A Utility that extracts either resource types or resource paths from a TypedRecordInstance. + * + * Its limitations are mostly around its intentional non-recursiveness. It presumes that APIs which + * implement includes will not allow cyclical include paths, and will collapse includes by type. + * + * This follows closer to the JSON:API fields spec than to the includes spec in nature, but in + * practice it is so impracticle for an API to allow z-algo include paths that this is probably + * reasonable. + * + * We may need to revisit this in the future, opting to either make this restriction optional or + * to allow for other strategies. + * + * There's a 90% chance this particular implementation belongs being in the JSON:API package instead + * of core-types, but it's here for now. + * + * @typedoc + */ +type ExtractUnion< + MAX_DEPTH extends _DEPTHCOUNT, + T extends TypedRecordInstance, + IncludePrefix extends boolean = false, + Ignore = NONE, + Pre = NONE, + DEPTH extends _DEPTHCOUNT = 1, +> = Exclude< + IncludePrefix extends true + ? // if we want to include prefix, we union with the prefix. Outer Exclude will filter any "NONE" types + _ExtractUnion | Pre + : // Else we just union the types. + _ExtractUnion | T[typeof Type], + NONE +>; + +type _DEPTHCOUNT = 1 | 2 | 3 | 4 | 5; +type INC_DEPTH = START extends 1 ? 2 : START extends 2 ? 3 : START extends 3 ? 4 : 5; +type IS_MAX_DEPTH< + DEPTH extends _DEPTHCOUNT, + MAX_DEPTH extends _DEPTHCOUNT = DEFAULT_MAX_DEPTH, +> = DEPTH extends MAX_DEPTH ? true : false; +type DEFAULT_MAX_DEPTH = 3; +/** + * A utility that provides the union of all ResourceName for all potential + * includes for the given TypedRecordInstance. + * + * @typedoc + */ +export type ExtractSuggestedCacheTypes< + T extends TypedRecordInstance, + MAX_DEPTH extends _DEPTHCOUNT = DEFAULT_MAX_DEPTH, +> = ExtractUnion; // ToPaths, false>; + +/** + * A utility that provides the union type of all valid include paths for the given + * TypedRecordInstance. + * + * Cyclical paths are filtered out. + * + * @typedoc + */ +export type Includes = ExtractUnion< + MAX_DEPTH, + T, + true +>; + +export type OpaqueRecordInstance = unknown; + +export type _StringSatisfiesIncludes = T extends SET + ? FT + : T extends `${infer U},${infer V}` + ? U extends SET + ? _StringSatisfiesIncludes, FT> + : never + : never; + +export type StringSatisfiesIncludes = _StringSatisfiesIncludes; + +export function createIncludeValidator() { + return function validateIncludes(includes: StringSatisfiesIncludes>): U { + return includes; + }; +} diff --git a/packages/core-types/src/record.type-test.ts b/packages/core-types/src/record.type-test.ts new file mode 100644 index 00000000000..73c4e973690 --- /dev/null +++ b/packages/core-types/src/record.type-test.ts @@ -0,0 +1,247 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// tests + +import type { ExtractSuggestedCacheTypes, Includes, StringSatisfiesIncludes, TypedRecordInstance } from './record'; +import { createIncludeValidator } from './record'; +import type { Type } from './symbols'; + +type NoRelations = { + name: string; + [Type]: 'no-relations'; +}; + +type NoSelfReference = { + name: string; + related: MyThing; + [Type]: 'no-self-reference'; +}; + +type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; +}; + +type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; +}; + +type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + reallyDeepThing: ReallyDeepThing; + [Type]: 'deep-thing'; +}; + +type ReallyDeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'really-deep-thing'; +}; + +function takesSuggestTypes( + types: ExtractSuggestedCacheTypes[] +) {} +takesSuggestTypes([ + 'thing', + 'other-thing', + 'deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +takesSuggestTypes([ + // we should include our own type even when not self-referential + 'no-self-reference', + 'thing', + 'other-thing', + // @ts-expect-error this should fail at recursion depth 3 + 'deep-thing', + // @ts-expect-error this should fail at recursion depth 4 + 'really-deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +takesSuggestTypes([ + // we should include our own type even when not self-referential + 'no-self-reference', + 'thing', + 'other-thing', + 'deep-thing', + // @ts-expect-error this should fail at recursion depth 4 + 'really-deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +takesSuggestTypes([ + // we should include our own type even when not self-referential + 'no-self-reference', + 'thing', + 'other-thing', + 'deep-thing', + 'really-deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +takesSuggestTypes([ + 'no-relations', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +function takesIncludes(includes: Includes[]) {} +takesIncludes([ + // @ts-expect-error not a valid path since it doesn't exist + 'not', + 'relatedThing', + 'relatedThings', + 'otherThing', + 'otherThings', + // @ts-expect-error not a valid path since its an attribute + 'name', + 'otherThing.thirdThing', + 'otherThing.deals', + 'otherThing.original', + // @ts-expect-error should not include this since original was already processed above + 'otherThing.original.relatedThing', + 'otherThing.deep', + 'otherThing.deep.relatedThing', + 'otherThing.deep.otherThing', + 'otherThing.deep.myThing', + 'otherThings.thirdThing', + 'otherThings.deals', + 'otherThings.original', + 'otherThings.deep', + 'otherThings.deep.relatedThing', + // @ts-expect-error should not include this since original was already processed above + 'otherThings.deep.relatedThing.relatedThing', + 'otherThings.deep.otherThing', + 'otherThings.deep.myThing', + 'otherThing.deep.reallyDeepThing', + // @ts-expect-error should not include this since depth is capped at 3 + 'otherThing.deep.reallyDeepThing.relatedThing', +]); + +takesIncludes([ + // @ts-expect-error not a valid path since it doesn't exist + 'not', +]); + +const validator = createIncludeValidator(); + +function expectString(t: string) {} +function expectNever(t: never) {} + +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); +expectString(validator('relatedThing,otherThing,otherThings.thirdThing')); + +expectNever( + // @ts-expect-error not a valid path since it doesn't exist + validator('not') +); +expectString(validator('relatedThing')); +expectString(validator('relatedThings')); +expectString(validator('otherThing')); +expectString(validator('otherThings')); +expectNever( + // @ts-expect-error not a valid path since its an attribute + validator('name') +); +expectString(validator('otherThing.thirdThing')); +expectString(validator('otherThing.deals')); +expectString(validator('otherThing.original')); +expectNever( + // @ts-expect-error should not include this since original was already processed above + validator('otherThing.original.relatedThing') +); +expectString(validator('otherThing.deep')); +expectString(validator('otherThing.deep.relatedThing')); +expectString(validator('otherThing.deep.otherThing')); +expectString(validator('otherThing.deep.myThing')); +expectString(validator('otherThings.thirdThing')); +expectString(validator('otherThings.deals')); +expectString(validator('otherThings.original')); +expectString(validator('otherThings.deep')); +expectString(validator('otherThings.deep.relatedThing')); + +expectNever( + // @ts-expect-error should not include this since original was already processed above + validator('otherThings.deep.relatedThing.relatedThing') +); +expectString(validator('otherThings.deep.otherThing')); +expectString(validator('otherThings.deep.myThing')); +expectString(validator('otherThing.deep.reallyDeepThing')); +expectNever( + // @ts-expect-error should not include this since depth is capped at 3 + validator('otherThing.deep.reallyDeepThing.relatedThing') +); + +type A = 'hello' | 'there' | 'goodnight' | 'moon'; + +type V1 = 'hello'; +type V2 = 'hello,there'; +type V3 = 'there,hello,goodnight'; +type V4 = 'moon,there'; +type V5 = 'moon,goodnight,hello,there'; +type V6 = 'hello,there,goodnight,moon'; + +type I1 = 'where'; +type I2 = 'hello,not'; +type I3 = 'invalid,hello,there'; +type I4 = 'hello,there,goodnight,moot'; +type I5 = 'hello,there,goodnight,moon,invalid'; +type I6 = 'hello,there,goodnight,moons'; + +function ExpectString(): V { + return '' as V; +} +function ExpectNever(): V { + return '' as V; +} + +ExpectString>(); +ExpectString>(); +ExpectString>(); +ExpectString>(); +ExpectString>(); +ExpectString>(); + +ExpectNever>(); +ExpectNever>(); +ExpectNever>(); +ExpectNever>(); +ExpectNever>(); +ExpectNever>(); + +const foo: StringSatisfiesIncludes< + 'otherThings.deep.relatedThing', + Includes +> = 'otherThings.deep.relatedThing'; + +// @ts-expect-error foo2 is never :) +const foo2: StringSatisfiesIncludes<'company,company.ceo,friends', Includes> = 'company,company.ceo,friends'; + +expectString(foo); +expectNever(foo2); diff --git a/packages/core-types/src/request.ts b/packages/core-types/src/request.ts new file mode 100644 index 00000000000..559e4e5ffa1 --- /dev/null +++ b/packages/core-types/src/request.ts @@ -0,0 +1,344 @@ +import { getOrSetGlobal, getOrSetUniversal } from './-private'; +import type { StableRecordIdentifier } from './identifier'; +import type { QueryParamsSerializationOptions } from './params'; +import type { ExtractSuggestedCacheTypes, Includes, TypedRecordInstance, TypeFromInstanceOrString } from './record'; +import type { ResourceIdentifierObject } from './spec/json-api-raw'; +import type { RequestSignature } from './symbols'; + +type Store = unknown; + +export const SkipCache = getOrSetUniversal('SkipCache', Symbol.for('wd:skip-cache')); +export const EnableHydration = getOrSetUniversal('EnableHydration', Symbol.for('wd:enable-hydration')); +export const IS_FUTURE = getOrSetGlobal('IS_FUTURE', Symbol('IS_FUTURE')); +export const STRUCTURED = getOrSetGlobal('DOC', Symbol('DOC')); + +export type HTTPMethod = 'GET' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'; + +/** + * Use these options to adjust CacheHandler behavior for a request. + * + * @typedoc + */ +export type CacheOptions = { + /** + * A key that uniquely identifies this request. If not present, the url wil be used + * as the key for any GET request, while all other requests will not be cached. + * + * @typedoc + */ + key?: string; + /** + * If true, the request will be made even if a cached response is present + * and not expired. + * + * @typedoc + */ + reload?: boolean; + /** + * If true, and a cached response is present and not expired, the request + * will be made in the background and the cached response will be returned. + * + * @typedoc + */ + backgroundReload?: boolean; + /** + * Useful for metadata around when to invalidate the cache. Typically used + * by strategies that invalidate requests by resource type when a new resource + * of that type has been created. See the CachePolicy implementation + * provided by `@ember-data/request-utils` for an example. + * + * It is recommended to only use this for query/queryRecord requests where + * new records created later would affect the results, though using it for + * findRecord requests is also supported if desired where it may be useful + * when a create may affect the result of a sideloaded relationship. + * + * Generally it is better to patch the cache directly for relationship updates + * than to invalidate findRecord requests for one. + * + * @typedoc + */ + types?: T extends TypedRecordInstance ? ExtractSuggestedCacheTypes[] : string[]; + + /** + * If true, the request will never be handled by the cache-manager and thus + * will never resolve from cache nor update the cache. + * + * Generally this is only used for legacy request that manage resource cache + * updates in a non-standard way via the LegacyNetworkHandler. + * + * @typedoc + */ + [SkipCache]?: boolean; +}; +export type FindRecordRequestOptions = { + url: string; + method: 'GET'; + headers: Headers; + cacheOptions?: CacheOptions; + op: 'findRecord'; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; +}; + +export type QueryRequestOptions = { + url: string; + method: 'GET'; + headers: Headers; + cacheOptions?: CacheOptions; + op: 'query'; + [RequestSignature]?: RT; +}; + +export type PostQueryRequestOptions = { + url: string; + method: 'POST' | 'QUERY'; + headers: Headers; + body: string; + cacheOptions: CacheOptions & { key: string }; + op: 'query'; + [RequestSignature]?: RT; +}; + +export type DeleteRequestOptions = { + url: string; + method: 'DELETE'; + headers: Headers; + op: 'deleteRecord'; + data: { + record: StableRecordIdentifier>; + }; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; +}; + +type ImmutableRequest = Readonly & { + readonly headers: ImmutableHeaders; + readonly records: [StableRecordIdentifier]; +}; + +export type UpdateRequestOptions = { + url: string; + method: 'PATCH' | 'PUT'; + headers: Headers; + op: 'updateRecord'; + data: { + record: StableRecordIdentifier>; + }; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; +}; + +export type CreateRequestOptions = { + url: string; + method: 'POST'; + headers: Headers; + op: 'createRecord'; + data: { + record: StableRecordIdentifier>; + }; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; +}; + +export type ImmutableDeleteRequestOptions = ImmutableRequest; +export type ImmutableUpdateRequestOptions = ImmutableRequest; +export type ImmutableCreateRequestOptions = ImmutableRequest; + +export type RemotelyAccessibleIdentifier = { + id: string; + type: T; + lid?: string; +}; + +export type ConstrainedRequestOptions = { + reload?: boolean; + backgroundReload?: boolean; + host?: string; + namespace?: string; + resourcePath?: string; + urlParamsSettings?: QueryParamsSerializationOptions; +}; + +export type FindRecordOptions = ConstrainedRequestOptions & { + include?: T extends TypedRecordInstance ? Includes[] : string | string[]; +}; + +export interface StructuredDataDocument { + [STRUCTURED]?: true; + /** + * @see {@link ImmutableRequestInfo} + * @typedoc + */ + request: ImmutableRequestInfo; + response: Response | ResponseInfo | null; + content: T; +} +export interface StructuredErrorDocument extends Error { + [STRUCTURED]?: true; + request: ImmutableRequestInfo; + response: Response | ResponseInfo | null; + error: string | object; + content?: T; +} +export type StructuredDocument = StructuredDataDocument | StructuredErrorDocument; + +/** + * JavaScript's native Request class. + * + * EmberData provides our own typings due to incompleteness in the native typings. + * + * @typedoc + */ +type Request = { + /** Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. + * @typedoc + */ + cache?: RequestCache; + /** Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. + * @typedoc + */ + credentials?: RequestCredentials; + /** Returns the kind of resource requested by request, e.g., "document" or "script". + * @typedoc + */ + destination?: RequestDestination; + /** Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. + * @typedoc + */ + headers?: Headers; + /** Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] + * @typedoc + */ + integrity?: string; + /** Returns a boolean indicating whether or not request can outlive the global in which it was created. + * @typedoc + */ + keepalive?: boolean; + /** Returns request's HTTP method, which is "GET" by default. + * @typedoc + */ + method?: HTTPMethod; + /** Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. + * @typedoc + */ + mode?: RequestMode; + /** Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. + * @typedoc + */ + redirect?: RequestRedirect; + /** Returns the referrer of request. Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the `Referer` header of the request being made. + * @typedoc + */ + referrer?: string; + /** Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. + * @typedoc + */ + referrerPolicy?: ReferrerPolicy; + /** Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. + * @typedoc + */ + signal?: AbortSignal; + /** Returns the URL of request as a string. + * @typedoc + */ + url?: string; + /** Any body that you want to add to your request. Note that a GET or HEAD request may not have a body. + * @typedoc + */ + body?: BodyInit | null; +}; + +export type ImmutableHeaders = Headers & { clone?(): Headers; toJSON(): [string, string][] }; + +/** + * Extends JavaScript's native {@link Request} object with additional + * properties specific to the RequestManager's capabilities. + * + * @typedoc + */ +export type RequestInfo = Request & { + /** + * If provided, used instead of the AbortController auto-configured for each request by the RequestManager + * + * @typedoc + */ + controller?: AbortController; + + /** + * @see {@link CacheOptions} + * @typedoc + */ + cacheOptions?: CacheOptions; + store?: Store; + + op?: string; + + /** + * The identifiers of the primary resources involved in the request + * (if any). This may be used by handlers to perform transactional + * operations on the store. + * + * @typedoc + */ + records?: StableRecordIdentifier[]; + + disableTestWaiter?: boolean; + /** + * data that a handler should convert into + * the query (GET) or body (POST). + * + * Note: It is recommended that builders set query params + * and body directly in most scenarios. + * + * @typedoc + */ + data?: Record; + /** + * options specifically intended for handlers + * to utilize to process the request + * + * @typedoc + */ + options?: Record; + + [RequestSignature]?: RT; +}; + +/** + * Immutable version of {@link RequestInfo}. This is what is passed to handlers. + * + * @typedoc + */ +export type ImmutableRequestInfo = Readonly, 'controller'>> & { + readonly cacheOptions?: Readonly>; + readonly headers?: ImmutableHeaders; + readonly data?: Readonly>; + readonly options?: Readonly>; + + /** Whether the request body has been read. + * @typedoc + */ + readonly bodyUsed?: boolean; +}; + +export interface ResponseInfo { + readonly headers: ImmutableHeaders; // to do, maybe not this? + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: ResponseType; + readonly url: string; +} + +export interface RequestContext { + /** + * @see {@link ImmutableRequestInfo} + * @typedoc + */ + request: ImmutableRequestInfo; + id: number; + + setStream(stream: ReadableStream | Promise): void; + setResponse(response: Response | ResponseInfo | null): void; +} diff --git a/packages/core-types/src/schema/concepts.ts b/packages/core-types/src/schema/concepts.ts new file mode 100644 index 00000000000..606deb57f5f --- /dev/null +++ b/packages/core-types/src/schema/concepts.ts @@ -0,0 +1,21 @@ +import type { StableRecordIdentifier } from '../identifier'; +import type { ObjectValue, Value } from '../json/raw'; +import type { OpaqueRecordInstance } from '../record'; +import type { Type } from '../symbols'; + +export type Transformation = { + serialize(value: PT, options: ObjectValue | null, record: OpaqueRecordInstance): T; + hydrate(value: T | undefined, options: ObjectValue | null, record: OpaqueRecordInstance): PT; + defaultValue?(options: ObjectValue | null, identifier: StableRecordIdentifier): T; + [Type]: string; +}; + +export type Derivation = { + [Type]: string; +} & ((record: R, options: FM, prop: string) => T); + +export type HashFn = { [Type]: string } & (( + data: T, + options: ObjectValue | null, + prop: string | null +) => string); diff --git a/packages/core-types/src/schema/fields.ts b/packages/core-types/src/schema/fields.ts new file mode 100644 index 00000000000..22b485841f2 --- /dev/null +++ b/packages/core-types/src/schema/fields.ts @@ -0,0 +1,886 @@ +import type { ObjectValue, PrimitiveValue } from '../json/raw'; + +/** + * A generic "field" that can be used to define + * primitive value fields. + * + * Replaces "attribute" for primitive value fields. + * Can also be used to eject from deep-tracking of + * objects or arrays. + * + * A major difference between "field" and "attribute" + * is that "type" points to a legacy transform on + * "attribute" that a serializer *might* use, while + * "type" points to a new-style transform on "field" + * that a record implmentation *must* use. + * + * @typedoc + */ +export type GenericField = { + kind: 'field'; + name: string; + /** + * the name of the transform to use, if any + * @typedoc + */ + type?: string; + /** + * Options to pass to the transform, if any + * + * Must comply to the specific transform's options + * schema. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * A field that can be used to alias one key to another + * key present in the cache version of the resource. + * + * Unlike DerivedField, an AliasField may write to its + * source when a record is in an editable mode. + * + * AliasFields may utilize a transform, specified by type, + * to pre/post process the field. + * + * An AliasField may also specify a `kind` via options. + * `kind` may be any other valid field kind other than + * + * - `@hash` + * - `@id` + * - `@local` + * - `derived` + * + * This allows an AliasField to rename any field in the cache. + * + * Alias fields are generally intended to be used to support migrating + * between different schemas, though there are times where they are useful + * as a form of advanced derivation when used with a transform. For instance, + * an AliasField could be used to expose both a string and a Date version of the + * same field, with both being capable of being written to. + * + * @typedoc + */ +export type AliasField = { + kind: 'alias'; + name: string; + type: null; // should always be null + + /** + * The field def for which this is an alias. + * + * @typedoc + */ + options: + | GenericField + | ObjectField + | SchemaObjectField + | ArrayField + | SchemaArrayField + | ResourceField + | CollectionField + | LegacyAttributeField + | LegacyBelongsToField + | LegacyHasManyField; +}; + +/** + * Represents a field whose value is the primary + * key of the resource. + * + * This allows any field to serve as the primary + * key while still being able to drive identity + * needs within the system. + * + * This is useful for resources that use for instance + * 'uuid', 'urn' or 'entityUrn' or 'primaryKey' as their + * primary key field instead of 'id'. + * + * @typedoc + */ +export type IdentityField = { + kind: '@id'; + + /** + * The name of the field that serves as the + * primary key for the resource. + * + * @typedoc + */ + name: string; +}; + +/** + * Represents a specialized field whose computed value + * will be used as the primary key of a schema-object + * for serializability and comparison purposes. + * + * This field functions similarly to derived fields in that + * it is non-settable, derived state but differs in that + * it is only able to compute off of cache state and is given + * no access to a record instance. + * + * This means that if a hashing function wants to compute its value + * taking into account transformations and derivations it must + * perform those itself. + * + * A schema-array can declare its "key" value to be `@hash` if + * a schema-object has such a field. + * + * Only one hash field is permittable per schema-object, and + * it should be placed in the `ResourceSchema`'s `@id` field + * in place of an `IdentityField`. + * + * @typedoc + */ +export type HashField = { + kind: '@hash'; + + /** + * The name of the field that serves as the + * hash for the resource. + * + * Only required if access to this value by + * the UI is desired, it can be `null` otherwise. + * + * @typedoc + */ + name: string | null; + + /** + * The name of a function to run to compute the hash. + * The function will only have access to the cached + * data for the record. + * + * @typedoc + */ + type: string; + + /** + * Any options that should be provided to the hash + * function. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * Represents a field whose value is a local + * value that is not stored in the cache, nor + * is it sent to the server. + * + * Local fields can be written to, and their + * value is both memoized and reactive (though + * not deep-tracked). + * + * Because their state is not derived from the cache + * data or the server, they represent a divorced + * uncanonical source of state. + * + * For this reason Local fields should be used sparingly. + * + * Currently, while we document this feature here, + * only allow our own SchemaRecord should utilize them + * and the feature should be considered private. + * + * Example use cases that drove the creation of local + * fields are states like `isDestroying` and `isDestroyed` + * which are specific to a record instance but not + * stored in the cache. We wanted to be able to drive + * these fields from schema the same as all other fields. + * + * Don't make us regret this decision. + * + * @typedoc + */ +export type LocalField = { + kind: '@local'; + name: string; + /** + * Not currently utilized, we are considering + * allowing transforms to operate on local fields + * + * @typedoc + */ + type?: string; + options?: { defaultValue?: PrimitiveValue }; +}; + +/** + * Represents a field whose value is an object + * with keys pointing to values that are primitive + * values. + * + * If values of the keys are not primitives, or + * if the key/value pairs have well-defined shape, + * use 'schema-object' instead. + * + * @typedoc + */ +export type ObjectField = { + kind: 'object'; + name: string; + + /** + * The name of a transform to pass the entire object + * through before displaying or serializing it. + * + * @typedoc + */ + type?: string; + + /** + * Options to pass to the transform, if any + * + * Must comply to the specific transform's options + * schema. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * Represents a field whose value is an object + * with a well-defined structure described by + * a non-resource schema. + * + * If the object's structure is not well-defined, + * use 'object' instead. + * + * @typedoc + */ +export type SchemaObjectField = { + kind: 'schema-object'; + name: string; + + /** + * The name of the schema that describes the + * structure of the object. + * + * These schemas + * + * @typedoc + */ + type: string; + + options?: { + /** + * Whether this SchemaObject is Polymorphic. + * + * If the SchemaObject is polymorphic, `options.type` must also be supplied. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * If the SchemaObject is Polymorphic, the key on the raw cache data to use + * as the "resource-type" value for the schema-object. + * + * Defaults to "type". + * + * @typedoc + */ + type?: string; + }; +}; + +/** + * Represents a field whose value is an array + * of primitive values. + * + * If the array's elements are not primitive + * values, use 'schema-array' instead. + * + * @typedoc + */ +export type ArrayField = { + kind: 'array'; + name: string; + + /** + * The name of a transform to pass each item + * in the array through before displaying or + * or serializing it. + * + * @typedoc + */ + type?: string; + + /** + * Options to pass to the transform, if any + * + * Must comply to the specific transform's options + * schema. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * Represents a field whose value is an array + * of objects with a well-defined structure + * described by a non-resource schema. + * + * If the array's elements are not well-defined, + * use 'array' instead. + * + * @typedoc + */ +export type SchemaArrayField = { + kind: 'schema-array'; + name: string; + + /** + * The name of the schema that describes the + * structure of the objects in the array. + * + * @typedoc + */ + type: string; + + /** + * Options for configuring the behavior of the + * SchemaArray. + * + * @typedoc + */ + + /** + * Options for configuring the behavior of the + * SchemaArray. + * + * @typedoc + */ + options?: { + /** + * Configures how the SchemaArray determines whether + * an object in the cache is the same as an object + * previously used to instantiate one of the schema-objects + * it contains. + * + * The default is `'@identity'`. + * + * Valid options are: + * + * - `'@identity'` (default) : the cached object's referential identity will be used. + * This may result in significant instability when resource data is updated from the API + * - `'@index'` : the cached object's index in the array will be used. + * This is only a good choice for arrays that rarely if ever change membership + * - `'@hash'` : will lookup the `@hash` function supplied in the ResourceSchema for + * The contained schema-object and use the computed result to determine and compare identity. + * - (string) : the name of a field to use as the key, only GenericFields (kind `field`) + * Are valid field names for this purpose. The cache state without transforms applied will be + * used when comparing values. The field value should be unique enough to guarantee two schema-objects + * of the same type will not collide. + * + * @typedoc + */ + key?: '@identity' | '@index' | '@hash' | string; + + /** + * Whether this SchemaArray is Polymorphic. + * + * If the SchemaArray is polymorphic, `options.type` must also be supplied. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * If the SchemaArray is Polymorphic, the key on the raw cache data to use + * as the "resource-type" value for the schema-object. + * + * Defaults to "type". + * + * @typedoc + */ + type?: string; + }; +}; + +/** + * Represents a field whose value is derived + * from other fields in the schema. + * + * The value is read-only, and is not stored + * in the cache, nor is it sent to the server. + * + * Usage of derived fields should be minimized + * to scenarios where the derivation is known + * to be safe. For instance, derivations that + * required fields that are not always loaded + * or that require access to related resources + * that may not be loaded should be avoided. + * + * @typedoc + */ +export type DerivedField = { + kind: 'derived'; + name: string; + + /** + * The name of the derivation to use. + * + * Derivations are functions that take the + * record, options, and the name of the field + * as arguments, and return the derived value. + * + * Derivations are memoized, and are only + * recomputed when the fields they depend on + * change. + * + * Derivations are not stored in the cache, + * and are not sent to the server. + * + * Derivation functions must be explicitly + * registered with the schema service. + * + * @typedoc + */ + type: string; + + /** + * Options to pass to the derivation, if any + * + * Must comply to the specific derivation's + * options schema. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * Represents a field that is a reference to + * another resource. + * + * @typedoc + */ +export type ResourceField = { + kind: 'resource'; + name: string; + + /** + * The name of the resource that this field + * refers to. In the case of a polymorphic + * relationship, this should be the trait + * or abstract type. + * + * @typedoc + */ + type: string; + + /** + * Options for resources are optional. If + * not present, all options are presumed + * to be falsey + * + * @typedoc + */ + options?: { + /** + * Whether the relationship is async + * + * If true, it is expected that the cache + * data for this field will contain a link + * that can be used to fetch the related + * resource when needed. + * + * @typedoc + */ + async?: boolean; + + /** + * The name of the inverse field on the + * related resource that points back to + * this field on this resource to form a + * bidirectional relationship. + * + * If null, the relationship is unidirectional. + * + * @typedoc + */ + inverse?: string | null; + + /** + * If this field is satisfying a polymorphic + * relationship on another resource, then this + * should be set to the trait or abstract type + * that this resource implements. + * + * @typedoc + */ + as?: string; + + /** + * Whether this field is a polymorphic relationship, + * meaning that it can point to multiple types of + * resources so long as they implement the trait + * or abstract type specified in `type`. + * + * @typedoc + */ + polymorphic?: boolean; + }; +}; + +/** + * Represents a field that is a reference to + * a collection of other resources, potentially + * paginate. + * + * @typedoc + */ +export type CollectionField = { + kind: 'collection'; + name: string; + + /** + * The name of the resource that this field + * refers to. In the case of a polymorphic + * relationship, this should be the trait + * or abstract type. + * + * @typedoc + */ + type: string; + + /** + * Options for resources are optional. If + * not present, all options are presumed + * to be falsey + * + * @typedoc + */ + options?: { + /** + * Whether the relationship is async + * + * If true, it is expected that the cache + * data for this field will contain links + * that can be used to fetch the related + * resources when needed. + * + * When false, it is expected that all related + * resources are loaded together with this resource, + * and that the cache data for this field will + * contain the full list of pointers. + * + * When true, it is expected that the relationship + * is paginated. If the relationship is not paginated, + * then the cache data for "page 1" would contain the + * full list of pointers, and loading "page 1" would + * load all related resources. + * + * @typedoc + */ + async?: boolean; + + /** + * The name of the inverse field on the + * related resource that points back to + * this field on this resource to form a + * bidirectional relationship. + * + * If null, the relationship is unidirectional. + * + * @typedoc + */ + inverse?: string | null; + + /** + * If this field is satisfying a polymorphic + * relationship on another resource, then this + * should be set to the trait or abstract type + * that this resource implements. + * + * @typedoc + */ + as?: string; + + /** + * Whether this field is a polymorphic relationship, + * meaning that it can point to multiple types of + * resources so long as they implement the trait + * or abstract type specified in `type`. + * + * @typedoc + */ + polymorphic?: boolean; + }; +}; + +/** + * > [!CAUTION] + * > This Field is LEGACY + * + * A generic "field" that can be used to define + * primitive value fields. + * + * If the field points to an object or array, + * it will not be deep-tracked. + * + * Transforms when defined are legacy transforms + * that a serializer *might* use, but their usage + * is not guaranteed. + * + * @typedoc + */ +export type LegacyAttributeField = { + kind: 'attribute'; + name: string; + /** + * The name of the transform to use, if any + * + * @typedoc + */ + type?: string | null; + /** + * Options to pass to the transform, if any + * + * Must comply to the specific transform's options + * schema. + * + * @typedoc + */ + options?: ObjectValue; +}; + +/** + * > [!CAUTION] + * > This Field is LEGACY + * + * Represents a field that is a reference to + * another resource. + * + * This is the legacy version of the `ResourceField`. + * + * @typedoc + */ +export type LegacyBelongsToField = { + kind: 'belongsTo'; + name: string; + + /** + * The name of the resource that this field + * refers to. In the case of a polymorphic + * relationship, this should be the trait + * or abstract type. + * + * @typedoc + */ + type: string; + + /** + * Options for belongsTo are mandatory. + * + * @typedoc + */ + options: { + /** + * Whether the relationship is async + * + * If true, it is expected that the cache + * data for this field will contain a link + * or a pointer that can be used to fetch + * the related resource when needed. + * + * Pointers are highly discouraged. + * + * @typedoc + */ + async: boolean; + + /** + * The name of the inverse field on the + * related resource that points back to + * this field on this resource to form a + * bidirectional relationship. + * + * If null, the relationship is unidirectional. + * + * @typedoc + */ + inverse: string | null; + + /** + * If this field is satisfying a polymorphic + * relationship on another resource, then this + * should be set to the trait or abstract type + * that this resource implements. + * + * @typedoc + */ + as?: string; + + /** + * Whether this field is a polymorphic relationship, + * meaning that it can point to multiple types of + * resources so long as they implement the trait + * or abstract type specified in `type`. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * When omitted, the cache data for this field will + * clear local state of all changes except for the + * addition of records still in the "new" state any + * time the remote data for this field is updated. + * + * When set to `false`, the cache data for this field + * will instead intelligently commit any changes from + * local state that are present in the remote data, + * leaving any remaining changes in local state still. + * + * @typedoc + */ + resetOnRemoteUpdate?: false; + }; +}; + +/** + * > [!CAUTION] + * > This Field is LEGACY + * + * Represents a field that is a reference to + * a collection of other resources. + * + * This is the legacy version of the `CollectionField`. + * + * @typedoc + */ +export type LegacyHasManyField = { + kind: 'hasMany'; + name: string; + type: string; + + /** + * Options for hasMany are mandatory. + * + * @typedoc + */ + options: { + /** + * Whether the relationship is async + * + * If true, it is expected that the cache + * data for this field will contain links + * or pointers that can be used to fetch + * the related resources when needed. + * + * When false, it is expected that all related + * resources are loaded together with this resource, + * and that the cache data for this field will + * contain the full list of pointers. + * + * hasMany relationships do not support pagination. + * + * @typedoc + */ + async: boolean; + + /** + * The name of the inverse field on the + * related resource that points back to + * this field on this resource to form a + * bidirectional relationship. + * + * If null, the relationship is unidirectional. + * + * @typedoc + */ + inverse: string | null; + + /** + * If this field is satisfying a polymorphic + * relationship on another resource, then this + * should be set to the trait or abstract type + * that this resource implements. + * + * @typedoc + */ + as?: string; + + /** + * Whether this field is a polymorphic relationship, + * meaning that it can point to multiple types of + * resources so long as they implement the trait + * or abstract type specified in `type`. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * When omitted, the cache data for this field will + * clear local state of all changes except for the + * addition of records still in the "new" state any + * time the remote data for this field is updated. + * + * When set to `false`, the cache data for this field + * will instead intelligently commit any changes from + * local state that are present in the remote data, + * leaving any remaining changes in local state still. + * + * @typedoc + */ + resetOnRemoteUpdate?: false; + }; +}; + +export type FieldSchema = + | GenericField + | AliasField + | LocalField + | ObjectField + | SchemaObjectField + | ArrayField + | SchemaArrayField + | DerivedField + | ResourceField + | CollectionField + | LegacyAttributeField + | LegacyBelongsToField + | LegacyHasManyField; + +export type ResourceSchema = { + legacy?: boolean; + /** + * For primary resources, this should be an IdentityField + * + * for schema-objects, this should be either a HashField or null + * + * @typedoc + */ + identity: IdentityField | HashField | null; + /** + * The name of the schema + * + * For cacheable resources, this should be the + * primary resource type. + * + * For object schemas, this should be the name + * of the object schema. object schemas should + * follow the following guidelines for naming + * + * - for globally shared objects: The pattern `$field:${KlassName}` e.g. `$field:AddressObject` + * - for resource-specific objects: The pattern `$${ResourceKlassName}:$field:${KlassName}` e.g. `$User:$field:ReusableAddress` + * - for inline objects: The pattern `$${ResourceKlassName}.${fieldPath}:$field:anonymous` e.g. `$User.shippingAddress:$field:anonymous` + * + * @typedoc + */ + type: string; + traits?: string[]; + fields: FieldSchema[]; +}; + +export type LegacyFieldSchema = LegacyAttributeField | LegacyBelongsToField | LegacyHasManyField; +export type LegacyRelationshipSchema = LegacyBelongsToField | LegacyHasManyField; diff --git a/packages/core-types/src/spec/document.ts b/packages/core-types/src/spec/document.ts new file mode 100644 index 00000000000..0ee5fa01399 --- /dev/null +++ b/packages/core-types/src/spec/document.ts @@ -0,0 +1,46 @@ +import type { StableExistingRecordIdentifier } from '../identifier'; +import type { ApiError } from './error'; +import type { Links, Meta, PaginationLinks } from './json-api-raw'; + +export interface ResourceMetaDocument { + // the url or cache-key associated with the structured document + lid?: string; + meta: Meta; + links?: Links | PaginationLinks; +} + +export interface SingleResourceDataDocument { + // the url or cache-key associated with the structured document + lid?: string; + links?: Links | PaginationLinks; + meta?: Meta; + data: T | null; + included?: T[]; +} + +export interface CollectionResourceDataDocument { + // the url or cache-key associated with the structured document + lid?: string; + links?: Links | PaginationLinks; + meta?: Meta; + data: T[]; + included?: T[]; +} + +export type ResourceDataDocument = + | SingleResourceDataDocument + | CollectionResourceDataDocument; + +export interface ResourceErrorDocument { + // the url or cache-key associated with the structured document + lid?: string; + links?: Links | PaginationLinks; + meta?: Meta; + errors: ApiError[]; +} + +export type ResourceDocument = + | ResourceMetaDocument + | SingleResourceDataDocument + | CollectionResourceDataDocument + | ResourceErrorDocument; diff --git a/packages/core-types/src/spec/error.ts b/packages/core-types/src/spec/error.ts new file mode 100644 index 00000000000..e34b838dbe5 --- /dev/null +++ b/packages/core-types/src/spec/error.ts @@ -0,0 +1,19 @@ +import type { Link, Meta } from './json-api-raw'; + +export interface ApiError { + id?: string; + title?: string; + detail?: string; + links?: { + about?: Link; + type?: Link; + }; + status?: string; + code?: string; + source?: { + pointer: string; + parameter?: string; + header?: string; + }; + meta?: Meta; +} diff --git a/packages/core-types/src/spec/json-api-raw.ts b/packages/core-types/src/spec/json-api-raw.ts new file mode 100644 index 00000000000..56c7142b8ea --- /dev/null +++ b/packages/core-types/src/spec/json-api-raw.ts @@ -0,0 +1,172 @@ +/* + @module @warp-drive/core-types +*/ +import type { ArrayValue, ObjectValue } from '../json/raw'; + +export type Meta = ObjectValue; +export type LinkObject = { href: string; meta?: Meta }; +export type Link = string | LinkObject; +export interface Links { + related?: Link | null; + self?: Link | null; +} +export interface PaginationLinks extends Links { + first?: Link | null; + last?: Link | null; + prev?: Link | null; + next?: Link | null; +} + +/** + * Serves as a reference to a `Resource` but does not contain + * any data itself. + * + * Used to establish relationship linkages between `Resources` and + * to address data that may not be available synchronously. + * + * [JSON:API Spec](https://jsonapi.org/format/#document-resource-identifier-objects) + * @internal + */ +export interface ExistingResourceIdentifierObject { + id: string; + type: T; + + /** + * While not officially part of the `JSON:API` spec, + * `ember-data` allows the use of `lid` as a local + * identifier for a `Resource`. + * + * @recommended It is best to include the lid used when creating + * a new resource if this is the response to a new resource creation, + * also recommended if this resource type uses secondary indexes. + * + * Once a `ResourceIdentifierObject` has been seen by the cache, `lid` + * should always be present. Only when inbound from the an `API` response + * is `lid` considered optional. + * + * [Identifiers RFC](https://github.com/emberjs/rfcs/blob/main/text/0403-ember-data-identifiers.md#ember-data--identifiers) + * @internal + */ + lid?: string; + + /** + * While valid in the `JSON:API` spec, + * `ember-data` ignores `meta` on `ResourceIdentifierObjects` + * + * @ignored this property goes un-utilized and will be lost + * @internal + */ + meta?: Meta; +} + +/** + * Serves as a reference to a resource created on the client + * but not yet persisted. + * + * @internal + */ +export interface NewResourceIdentifierObject { + /** + * Resources newly created on the client _may_ + * not have an `id` available to them prior + * to completion of their first successful `save`. + * + * `id` will be `null` in this case. + * + * @internal + */ + id: string | null; + type: T; + + /** + * Resources newly created on the client _will always_ + * have an `lid` assigned immediately and available. + * @internal + */ + lid: string; +} + +export interface ResourceIdentifier { + lid: string; +} + +export type ResourceIdentifierObject = + | ResourceIdentifier + | ExistingResourceIdentifierObject + | NewResourceIdentifierObject; + +// TODO disallow NewResource, make narrowable +export interface SingleResourceRelationship { + data?: T | null; + meta?: Meta; + links?: Links; +} + +export interface CollectionResourceRelationship { + data?: T[]; + meta?: Meta; + links?: PaginationLinks; +} + +export type InnerRelationshipDocument = + | SingleResourceRelationship + | CollectionResourceRelationship; + +export type ResourceRelationshipsObject = Record< + string, + InnerRelationshipDocument +>; + +/** + * Contains the data for an existing resource in JSON:API format + * @internal + */ +export type ExistingResourceObject = ExistingResourceIdentifierObject & { + meta?: Meta; + attributes?: ObjectValue; + relationships?: ResourceRelationshipsObject; + links?: Links; +}; + +export type NewResourceObject = NewResourceIdentifierObject & { + meta?: Meta; + attributes?: ObjectValue; + relationships?: ResourceRelationshipsObject; + links?: Links; +}; + +export type ResourceObject = ExistingResourceObject | NewResourceObject; + +type Document = { + lid?: string; + meta?: Meta; + included?: ExistingResourceObject[]; + jsonapi?: ObjectValue; + links?: Links | PaginationLinks; + errors?: ArrayValue; +}; + +export type EmptyResourceDocument = Document & { + data: null; +}; + +export type SingleResourceDocument = Document & { + data: ExistingResourceObject; +}; + +export type CollectionResourceDocument = Document & { + data: ExistingResourceObject[]; +}; + +/** + * A (RAW) JSON:API Formatted Document. + * + * These documents should follow the JSON:API spec but do not + * have the same level of guarantees as their `spec` counterparts. + * + * @internal + */ +export type JsonApiDocument = + | EmptyResourceDocument + | SingleResourceDocument + | CollectionResourceDocument; diff --git a/packages/core-types/src/symbols.ts b/packages/core-types/src/symbols.ts new file mode 100644 index 00000000000..3e6c58827a3 --- /dev/null +++ b/packages/core-types/src/symbols.ts @@ -0,0 +1,91 @@ +import { getOrSetGlobal } from './-private'; + +/* + * @module @warp-drive/core-types + */ +export const RecordStore = getOrSetGlobal('Store', Symbol('Store')); + +/** + * Symbol for the name of a resource, transformation + * or derivation. + * + * ### With Resources + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * ### With Derivations + * + * Required for derivations registered with + * `store.registerDerivation(derivation)`. + * + * ```ts + * function concat(record: object, options: ObjectValue | null, prop: string): string {} + * concat[Name] = 'concat'; + * ``` + * + * ### With Transforms + * + * Required for new-style transformations registered + * with `store.registerTransform(transform)`. + * + * For legacy transforms, if not used, + * `attr('name')` will allow any string name. + * `attr('name')` will always allow any string name. + * + * If used, `attr('name')` will enforce + * that the name is the same as the transform name. + * + * @type {Symbol} + * @typedoc + */ +export const Type = getOrSetGlobal('$type', Symbol('$type')); + +/** + * Symbol for the type of a resource. + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * @type {Symbol} + * @typedoc + */ +export const ResourceType = Type; + +/** + * Symbol for the name of a transform. + * + * This is an optional feature that can be used by + * transform implementations to provide a typescript + * hint for the name of the transform. + * + * If not used, `attr('name')` will + * allow any string name. `attr('name')` will always + * allow any string name. + * + * If used, `attr('name')` will enforce + * that the name is the same as the transform name. + * + * @type {Symbol} + * @typedoc + */ +export const TransformName = Type; + +/** + * Symbol for use by builders to indicate the return type + * generic to use for store.request() + * + * @type {Symbol} + * @typedoc + */ +export const RequestSignature = getOrSetGlobal('RequestSignature', Symbol('RequestSignature')); diff --git a/packages/core-types/src/utils.ts b/packages/core-types/src/utils.ts new file mode 100644 index 00000000000..96bd008d7b9 --- /dev/null +++ b/packages/core-types/src/utils.ts @@ -0,0 +1 @@ +export type WithPartial = Omit & Partial>; diff --git a/packages/core-types/tsconfig.json b/packages/core-types/tsconfig.json new file mode 100644 index 00000000000..2ce7e7f7556 --- /dev/null +++ b/packages/core-types/tsconfig.json @@ -0,0 +1,51 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + // Support generation of source maps. Note: you must *also* enable source + // maps in your `ember-cli-babel` config and/or `babel.config.js`. + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "baseUrl": ".", + "paths": { + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../build-config" + } + ] +} diff --git a/packages/core-types/vite.config.mjs b/packages/core-types/vite.config.mjs new file mode 100644 index 00000000000..32e97b83e74 --- /dev/null +++ b/packages/core-types/vite.config.mjs @@ -0,0 +1,29 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = []; + +export const entryPoints = [ + './src/cache/**.ts', + './src/json/**.ts', + './src/schema/**.ts', + './src/spec/**.ts', + './src/cache.ts', + './src/graph.ts', + './src/identifier.ts', + './src/index.ts', + './src/params.ts', + './src/record.ts', + './src/request.ts', + './src/symbols.ts', + './src/utils.ts', + // non-public + './src/-private.ts', +]; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/debug/.npmignore b/packages/debug/.npmignore deleted file mode 100644 index e4bce62a5ec..00000000000 --- a/packages/debug/.npmignore +++ /dev/null @@ -1,40 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/node-tests - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/debug/CHANGELOG.md b/packages/debug/CHANGELOG.md new file mode 100644 index 00000000000..6393b61144b --- /dev/null +++ b/packages/debug/CHANGELOG.md @@ -0,0 +1,63 @@ +# @ember-data/debug Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@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)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +## 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)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9070](https://github.com/emberjs/data/pull/9070) docs: fix note notation to make use of github formatting ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#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)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/debug/README.md b/packages/debug/README.md index bacae7a5da7..a4dad8cd76c 100644 --- a/packages/debug/README.md +++ b/packages/debug/README.md @@ -3,12 +3,23 @@ Provides ember-inspector support for Ember apps built with EmberData ## Installation -> **Note** If using `ember-data`, this library comes pre-installed. +> **Note** +> If using `ember-data`, this library comes pre-installed. ``` pnpm install @ember-data/debug ``` +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/debug/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/debug/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/debug/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/debug/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/debug/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + + ## Usage ### removing inspector support in production diff --git a/packages/debug/addon-main.cjs b/packages/debug/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/debug/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/debug/addon/index.js b/packages/debug/addon/index.js deleted file mode 100644 index 52f0433fbaa..00000000000 --- a/packages/debug/addon/index.js +++ /dev/null @@ -1,311 +0,0 @@ -/** - # Overview - - This package provides the `DataAdapter` which the [Ember Inspector](https://github.com/emberjs/ember-inspector) - uses to subscribe and retrieve information for the `data` tab in the inspector. - - This package adds roughly .6 KB when minified and compressed to your application in production; however, - you can opt out of shipping this addon in production via options in `ember-cli-build.js` - - ```js - let app = new EmberApp(defaults, { - emberData: { - includeDataAdapterInProduction: false - } - }); - ``` - - When using `ember-data` as a dependency of your app, the default is to ship the inspector support to production. - - When not using `ember-data` as a dependency but instead using EmberData via declaring specific `@ember-data/` - dependencies the default is to not ship to production. - - @module @ember-data/debug - @main @ember-data/debug -*/ -import { A } from '@ember/array'; -import { assert } from '@ember/debug'; -import DataAdapter from '@ember/debug/data-adapter'; -import { addObserver, removeObserver } from '@ember/object/observers'; -import { inject as service } from '@ember/service'; -import { capitalize, underscore } from '@ember/string'; -import { next } from '@ember/runloop'; - -import { typesMapFor } from './setup'; - -/** - Implements `@ember/debug/data-adapter` with for EmberData - integration with the ember-inspector. - - @class InspectorDataAdapter - @extends DataAdapter - @private -*/ -export default DataAdapter.extend({ - store: service('store'), - - /** - Specifies how records can be filtered based on the state of the record - Records returned will need to have a `filterValues` - property with a key for every name in the returned array - - @method getFilters - @private - @return {Array} List of objects defining filters - The object should have a `name` and `desc` property - */ - getFilters() { - return [ - { name: 'isNew', desc: 'New' }, - { name: 'isModified', desc: 'Modified' }, - { name: 'isClean', desc: 'Clean' }, - ]; - }, - - _nameToClass(type) { - return this.store.modelFor(type); - }, - - /** - Fetch the model types and observe them for changes. - Maintains the list of model types without needing the Model package for detection. - - @method watchModelTypes - @private - @param {Function} typesAdded Callback to call to add types. - Takes an array of objects containing wrapped types (returned from `wrapModelType`). - @param {Function} typesUpdated Callback to call when a type has changed. - Takes an array of objects containing wrapped types. - @return {Function} Method to call to remove all observers - */ - watchModelTypes(typesAdded, typesUpdated) { - const { store } = this; - const __getResourceCache = store._instanceCache.getResourceCache; - const _releaseMethods = []; - const discoveredTypes = typesMapFor(store); - - // Add any models that were added during initialization of the app, before the inspector was opened - discoveredTypes.forEach((_, type) => { - this.watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, _releaseMethods); - }); - - // Overwrite _createRecordData so newly added models will get added to the list - store._instanceCache.getResourceCache = (identifier) => { - // defer to ensure first-create does not result in an infinite loop, see https://github.com/emberjs/data/issues/8006 - next(() => this.watchTypeIfUnseen(store, discoveredTypes, identifier.type, typesAdded, typesUpdated, _releaseMethods)); - return __getResourceCache.call(store._instanceCache, identifier); - }; - - let release = () => { - _releaseMethods.forEach((fn) => fn()); - store._instanceCache.getResourceCache = __getResourceCache; - // reset the list so the models can be added if the inspector is re-opened - // the entries are set to false instead of removed, since the models still exist in the app - // we just need the inspector to become aware of them - discoveredTypes.forEach((value, key) => { - discoveredTypes.set(key, false); - }); - this.releaseMethods.removeObject(release); - }; - this.releaseMethods.pushObject(release); - return release; - }, - - /** - * Loop over the discovered types and use the callbacks from watchModelTypes to notify - * the consumer of this adapter about the mdoels. - * - * @method watchTypeIfUnseen - * @param {store} store - * @param {Map} discoveredTypes - * @param {String} type - * @param {Function} typesAdded - * @param {Function} typesUpdated - * @param {Array} releaseMethods - * @private - */ - watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, releaseMethods) { - if (discoveredTypes.get(type) !== true) { - let klass = store.modelFor(type); - let wrapped = this.wrapModelType(klass, type); - releaseMethods.push(this.observeModelType(type, typesUpdated)); - typesAdded([wrapped]); - discoveredTypes.set(type, true); - } - }, - - /** - Creates a human readable string used for column headers - - @method columnNameToDesc - @private - @param {String} name The attribute name - @return {String} Human readable string based on the attribute name - */ - columnNameToDesc(name) { - return capitalize(underscore(name).replace(/_/g, ' ').trim()); - }, - - /** - Get the columns for a given model type - - @method columnsForType - @private - @param {Model} typeClass - @return {Array} An array of columns of the following format: - name: {String} The name of the column - desc: {String} Humanized description (what would show in a table column name) - */ - columnsForType(typeClass) { - let columns = [ - { - name: 'id', - desc: 'Id', - }, - ]; - let count = 0; - let self = this; - typeClass.attributes.forEach((meta, name) => { - if (count++ > self.attributeLimit) { - return false; - } - let desc = this.columnNameToDesc(name); - columns.push({ name: name, desc: desc }); - }); - return columns; - }, - - /** - Fetches all loaded records for a given type - - @method getRecords - @private - @param {Model} modelClass of the record - @param {String} modelName of the record - @return {Array} An array of Model records - This array will be observed for changes, - so it should update when new records are added/removed - */ - getRecords(modelClass, modelName) { - if (arguments.length < 2) { - // Legacy Ember.js < 1.13 support - let containerKey = modelClass._debugContainerKey; - if (containerKey) { - let match = containerKey.match(/model:(.*)/); - if (match !== null) { - modelName = match[1]; - } - } - } - assert('Cannot find model name. Please upgrade to Ember.js >= 1.13 for Ember Inspector support', !!modelName); - return this.store.peekAll(modelName); - }, - - /** - Gets the values for each column - This is the attribute values for a given record - - @method getRecordColumnValues - @private - @param {Model} record to get values from - @return {Object} Keys should match column names defined by the model type - */ - getRecordColumnValues(record) { - let count = 0; - let columnValues = { id: record.id }; - - record.eachAttribute((key) => { - if (count++ > this.attributeLimit) { - return false; - } - columnValues[key] = record[key]; - }); - return columnValues; - }, - - /** - Returns keywords to match when searching records - - @method getRecordKeywords - @private - @param {Model} record - @return {Array} Relevant keywords for search based on the record's attribute values - */ - getRecordKeywords(record) { - let keywords = []; - let keys = A(['id']); - record.eachAttribute((key) => keys.push(key)); - keys.forEach((key) => keywords.push(record[key])); - return keywords; - }, - - /** - Returns the values of filters defined by `getFilters` - These reflect the state of the record - - @method getRecordFilterValues - @private - @param {Model} record - @return {Object} The record state filter values - */ - getRecordFilterValues(record) { - return { - isNew: record.isNew, - isModified: record.hasDirtyAttributes && !record.isNew, - isClean: !record.hasDirtyAttributes, - }; - }, - - /** - Returns a color that represents the record's state - Possible colors: black, blue, green - - @method getRecordColor - @private - @param {Model} record - @return {String} The record color - */ - getRecordColor(record) { - let color = 'black'; - if (record.isNew) { - color = 'green'; - } else if (record.hasDirtyAttributes) { - color = 'blue'; - } - return color; - }, - - /** - Observes all relevant properties and re-sends the wrapped record - when a change occurs - - @method observeRecord - @private - @param {Model} record - @param {Function} recordUpdated Callback used to notify changes - @return {Function} The function to call to remove all observers - */ - observeRecord(record, recordUpdated) { - let releaseMethods = A(); - let keysToObserve = A(['id', 'isNew', 'hasDirtyAttributes']); - - record.eachAttribute((key) => keysToObserve.push(key)); - let adapter = this; - - keysToObserve.forEach(function (key) { - let handler = function () { - recordUpdated(adapter.wrapRecord(record)); - }; - addObserver(record, key, handler); - releaseMethods.push(function () { - removeObserver(record, key, handler); - }); - }); - - let release = function () { - releaseMethods.forEach((fn) => fn()); - }; - - return release; - }, -}); diff --git a/packages/debug/addon/setup.js b/packages/debug/addon/setup.js deleted file mode 100644 index 0badb69fe16..00000000000 --- a/packages/debug/addon/setup.js +++ /dev/null @@ -1,50 +0,0 @@ - -// dirty hack to add the known models to the typesMap -import Store from '@ember-data/store'; - -const StoreTypesMap = new WeakMap(); - -export function typesMapFor(store) { - let typesMap = StoreTypesMap.get(store); - - if (typesMap === undefined) { - typesMap = new Map(); - StoreTypesMap.set(store, typesMap); - } - - return typesMap; -} - -// EmberData 4.7+ -Object.defineProperty(Store.prototype, '_instanceCache', { - get() { - return this.__instanceCache; - }, - set(value) { - const getResourceCache = value.getResourceCache; - const store = this; - value.getResourceCache = function(identifier) { - const typesMap = typesMapFor(store); - if (!typesMap.has(identifier.type)) { - typesMap.set(identifier.type, false); - } - return getResourceCache.call(this, identifier); - } - this.__instanceCache = value; - } -}); - -// EmberData <= 4.6 -const __createRecordData = Store.prototype._createRecordData; -Store.prototype._createRecordData = function (identifier) { - const typesMap = typesMapFor(this); - if (!typesMap.has(identifier.type)) { - typesMap.set(identifier.type, false); - } - return __createRecordData.call(this, identifier); -}; - -export default { - name: '@ember-data/data-adapter', - initialize() {}, -}; diff --git a/packages/debug/app/data-adapter.js b/packages/debug/app/data-adapter.js deleted file mode 100644 index bd4bf31f7af..00000000000 --- a/packages/debug/app/data-adapter.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@ember-data/debug'; diff --git a/packages/debug/app/initializers/ember-data-data-adapter.js b/packages/debug/app/initializers/ember-data-data-adapter.js deleted file mode 100644 index 67b023ec8f2..00000000000 --- a/packages/debug/app/initializers/ember-data-data-adapter.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@ember-data/debug/setup'; diff --git a/packages/debug/babel.config.mjs b/packages/debug/babel.config.mjs new file mode 100644 index 00000000000..c23b859273f --- /dev/null +++ b/packages/debug/babel.config.mjs @@ -0,0 +1,12 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ['module:decorator-transforms', { runtime: { import: 'decorator-transforms/runtime' } }], + ], +}; diff --git a/packages/debug/ember-data-logo-dark.svg b/packages/debug/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/debug/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/debug/ember-data-logo-light.svg b/packages/debug/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/debug/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/debug/eslint.config.mjs b/packages/debug/eslint.config.mjs new file mode 100644 index 00000000000..a296abbeddf --- /dev/null +++ b/packages/debug/eslint.config.mjs @@ -0,0 +1,23 @@ +// @ts-check +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 [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: externals.slice(), + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/debug/index.js b/packages/debug/index.js deleted file mode 100644 index 6bc78abbd43..00000000000 --- a/packages/debug/index.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -const addonBuildConfigForDataPackage = require('@ember-data/private-build-infra/src/addon-build-config-for-data-package'); - -const addonBaseConfig = addonBuildConfigForDataPackage(require('./package.json')); - -module.exports = Object.assign({}, addonBaseConfig, { - shouldRollupPrivate: false, - __isEnabled: null, - externalDependenciesForPrivateModule() { - return []; - }, - treeFor() { - // Nested addons don't call isEnabled automatically, - // So this ensures that we return empty trees whenever - // we are not enabled. - if (this.isEnabled()) { - return this._super.treeFor.call(this, ...arguments); - } - }, - isEnabled() { - if (this.__isEnabled !== null) { - return this.__isEnabled; - } - const options = this.getEmberDataConfig(); - const env = process.env.EMBER_ENV; - - const parentIsEmberDataAddon = this.parent.pkg.name === 'ember-data'; - - if (options.includeDataAdapterInProduction === undefined) { - options.includeDataAdapterInProduction = parentIsEmberDataAddon; - } - - this.__isEnabled = env !== 'production' || options.includeDataAdapterInProduction === true; - - return this.__isEnabled; - }, -}); diff --git a/packages/debug/package.json b/packages/debug/package.json index da8590f7f8f..3a17493dd4b 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -13,34 +13,104 @@ "license": "MIT", "author": "", "directories": {}, - "scripts": {}, + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "files": [ + "unstable-preview-types", + "addon-main.cjs", + "dist", + "README.md", + "LICENSE.md", + "CHANGELOG.md", + "ember-data-logo-dark.svg", + "ember-data-logo-light.svg" + ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "peerDependencies": { - "@ember/string": "^3.0.1 || ^4.0.0", - "@ember-data/store": "workspace:4.12.8" + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/store": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@warp-drive/core-types": "workspace:*" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@ember-data/store": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "ember-auto-import": "^2.6.1", - "ember-cli-babel": "^7.26.11" + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@ember/string": "^4.0.0", - "@ember-data/store": "workspace:4.12.8", - "webpack": "^5.77.0" + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/request": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/test-waiters": "^3.1.0", + "@glimmer/component": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "decorator-transforms": "^2.2.2", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" + }, + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2, + "app-js": { + "./data-adapter.js": "./dist/_app_/data-adapter.js" + } }, - "ember-addon": {}, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/debug/src/_app_/data-adapter.js b/packages/debug/src/_app_/data-adapter.js new file mode 100644 index 00000000000..88a9cab288a --- /dev/null +++ b/packages/debug/src/_app_/data-adapter.js @@ -0,0 +1 @@ +export { default } from '@ember-data/debug/data-adapter'; diff --git a/packages/debug/src/data-adapter.ts b/packages/debug/src/data-adapter.ts new file mode 100644 index 00000000000..70aeb536583 --- /dev/null +++ b/packages/debug/src/data-adapter.ts @@ -0,0 +1,445 @@ +/** + # Overview + + This package provides the `DataAdapter` which the [Ember Inspector](https://github.com/emberjs/ember-inspector) + uses to subscribe and retrieve information for the `data` tab in the inspector. + + This package adds roughly .6 KB when minified and compressed to your application in production; however, + you can opt out of shipping this addon in production via options in `ember-cli-build.js` + + ```js + let app = new EmberApp(defaults, { + emberData: { + includeDataAdapterInProduction: false + } + }); + ``` + + When using `ember-data` as a dependency of your app, the default is to ship the inspector support to production. + + When not using `ember-data` as a dependency but instead using EmberData via declaring specific `@ember-data/` + dependencies the default is to not ship to production. + + @module @ember-data/debug + @main @ember-data/debug +*/ +import type { NativeArray } from '@ember/array'; +import { A } from '@ember/array'; +import DataAdapter from '@ember/debug/data-adapter'; +import { addObserver, removeObserver } from '@ember/object/observers'; +import { inject as service } from '@ember/service'; + +import { getGlobalConfig, macroCondition } from '@embroider/macros'; + +import type Model from '@ember-data/model'; +import { capitalize, underscore } from '@ember-data/request-utils/string'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; + +const StoreTypesMap = new WeakMap>(); + +type RecordColor = 'black' | 'red' | 'blue' | 'green'; +type Column = { + name: string; + desc: string; +}; +type WrappedType = { + name: N; + count: number; + columns: Column[]; + object: unknown; +}; +type WrappedRecord = { + object: T; + columnValues: object; + searchKeywords: unknown[]; + filterValues: object; + color: RecordColor | null; +}; +type WrappedTypeCallback = (types: WrappedType[]) => void; + +function debugInfo(this: Model) { + const relationships: { belongsTo?: []; hasMany?: [] } = {}; + const expensiveProperties: string[] = []; + + const identifier = recordIdentifierFor(this); + const fields = this.store.schema.fields(identifier); + + const attrGroup = { + name: 'Attributes', + properties: ['id'], + expand: true, + }; + const attributes = attrGroup.properties; + const groups = [attrGroup]; + + for (const field of fields.values()) { + switch (field.kind) { + case 'attribute': + attributes.push(field.name); + break; + case 'belongsTo': + case 'hasMany': { + let properties: string[] | undefined = relationships[field.kind]; + + if (properties === undefined) { + properties = relationships[field.kind] = []; + groups.push({ + name: field.kind, + properties, + expand: true, + }); + } + properties.push(field.name); + expensiveProperties.push(field.name); + break; + } + } + } + + groups.push({ + name: 'Flags', + properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], + expand: false, + }); + + return { + propertyInfo: { + // include all other mixins / properties (not just the grouped ones) + includeOtherProperties: true, + groups: groups, + // don't pre-calculate unless cached + expensiveProperties: expensiveProperties, + }, + }; +} + +function installDebugInfo(ModelKlass: typeof Model) { + /** + Provides info about the model for debugging purposes + by grouping the properties into more semantic groups. + + Meant to be used by debugging tools such as the Chrome Ember Extension. + + - Groups all attributes in "Attributes" group. + - Groups all belongsTo relationships in "Belongs To" group. + - Groups all hasMany relationships in "Has Many" group. + - Groups all flags in "Flags" group. + - Flags relationship CPs as expensive properties. + + @internal + */ + (ModelKlass.prototype as unknown as { _debugInfo: typeof debugInfo })._debugInfo = debugInfo; +} + +function typesMapFor(store: Store): Map { + let typesMap = StoreTypesMap.get(store); + + if (typesMap === undefined) { + typesMap = new Map(); + StoreTypesMap.set(store, typesMap); + } + + return typesMap; +} + +/** + Implements `@ember/debug/data-adapter` with for EmberData + integration with the ember-inspector. + + @class InspectorDataAdapter + @extends DataAdapter + @private +*/ +class InspectorDataAdapter extends DataAdapter { + @service('store') declare store: Store; + + /** + Specifies how records can be filtered based on the state of the record + Records returned will need to have a `filterValues` + property with a key for every name in the returned array + + @method getFilters + @private + @return {Array} List of objects defining filters + The object should have a `name` and `desc` property + */ + getFilters() { + return [ + { name: 'isNew', desc: 'New' }, + { name: 'isModified', desc: 'Modified' }, + { name: 'isClean', desc: 'Clean' }, + ]; + } + + _nameToClass(type: string) { + return this.store.modelFor(type); + } + + /** + Fetch the model types and observe them for changes. + Maintains the list of model types without needing the Model package for detection. + + @method watchModelTypes + @private + @param {Function} typesAdded Callback to call to add types. + Takes an array of objects containing wrapped types (returned from `wrapModelType`). + @param {Function} typesUpdated Callback to call when a type has changed. + Takes an array of objects containing wrapped types. + @return {Function} Method to call to remove all observers + */ + watchModelTypes(typesAdded: WrappedTypeCallback, typesUpdated: WrappedTypeCallback) { + const { store } = this; + + const discoveredTypes = typesMapFor(store); + const unsub = store.notifications.subscribe('resource', (identifier, notificationType) => { + if (notificationType === 'added') { + this.watchTypeIfUnseen(store, discoveredTypes, identifier.type, typesAdded, typesUpdated, _releaseMethods); + } + }); + + const _releaseMethods = [ + () => { + store.notifications.unsubscribe(unsub); + }, + ]; + + Object.keys(store.identifierCache._cache.resourcesByType).forEach((type) => { + discoveredTypes.set(type, false); + }); + + // Add any models that were added during initialization of the app, before the inspector was opened + discoveredTypes.forEach((_, type) => { + this.watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, _releaseMethods); + }); + + const release = () => { + _releaseMethods.forEach((fn) => fn()); + // reset the list so the models can be added if the inspector is re-opened + // the entries are set to false instead of removed, since the models still exist in the app + // we just need the inspector to become aware of them + discoveredTypes.forEach((value, key) => { + discoveredTypes.set(key, false); + }); + this.releaseMethods.removeObject(release); + }; + this.releaseMethods.pushObject(release); + return release; + } + + /** + * Loop over the discovered types and use the callbacks from watchModelTypes to notify + * the consumer of this adapter about the mdoels. + * + * @method watchTypeIfUnseen + * @param {store} store + * @param {Map} discoveredTypes + * @param {String} type + * @param {Function} typesAdded + * @param {Function} typesUpdated + * @param {Array} releaseMethods + * @private + */ + watchTypeIfUnseen( + store: Store, + discoveredTypes: Map, + type: string, + typesAdded: WrappedTypeCallback, + typesUpdated: WrappedTypeCallback, + releaseMethods: Array<() => void> + ) { + if (discoveredTypes.get(type) !== true) { + const klass = store.modelFor(type); + installDebugInfo(klass as typeof Model); + const wrapped = this.wrapModelType(klass, type); + releaseMethods.push(this.observeModelType(type, typesUpdated)); + typesAdded([wrapped]); + discoveredTypes.set(type, true); + } + } + + /** + Creates a human readable string used for column headers + + @method columnNameToDesc + @private + @param {String} name The attribute name + @return {String} Human readable string based on the attribute name + */ + columnNameToDesc(name: string) { + return capitalize(underscore(name).replace(/_/g, ' ').trim()); + } + + /** + Get the columns for a given model type + + @method columnsForType + @private + @param {Model} typeClass + @return {Array} An array of columns of the following format: + name: {String} The name of the column + desc: {String} Humanized description (what would show in a table column name) + */ + columnsForType(typeClass: ModelSchema) { + const columns = [ + { + name: 'id', + desc: 'Id', + }, + ]; + let count = 0; + typeClass.attributes.forEach((meta, name) => { + if (count++ > this.attributeLimit) { + return false; + } + const desc = this.columnNameToDesc(name); + columns.push({ name: name, desc: desc }); + }); + return columns; + } + + /** + Fetches all loaded records for a given type + + @method getRecords + @private + @param {Model} modelClass of the record + @param {String} modelName of the record + @return {Array} An array of Model records + This array will be observed for changes, + so it should update when new records are added/removed + */ + getRecords(modelClass: ModelSchema, modelName: string) { + if (arguments.length < 2) { + // Legacy Ember.js < 1.13 support + const containerKey = (modelClass as unknown as { _debugContainerKey?: string })._debugContainerKey; + if (containerKey) { + const match = containerKey.match(/model:(.*)/); + if (match !== null) { + modelName = match[1]; + } + } + } + assert('Cannot find model name. Please upgrade to Ember.js >= 1.13 for Ember Inspector support', !!modelName); + return this.store.peekAll(modelName) as unknown as NativeArray; + } + + /** + Gets the values for each column + This is the attribute values for a given record + + @method getRecordColumnValues + @private + @param {Model} record to get values from + @return {Object} Keys should match column names defined by the model type + */ + getRecordColumnValues(record: T) { + let count = 0; + const columnValues: Record = { id: record.id }; + + record.eachAttribute((key) => { + if (count++ > this.attributeLimit) { + return false; + } + columnValues[key] = record[key as keyof T]; + }); + return columnValues; + } + + /** + Returns keywords to match when searching records + + @method getRecordKeywords + @private + @param {Model} record + @return {Array} Relevant keywords for search based on the record's attribute values + */ + getRecordKeywords(record: T): NativeArray { + const keywords: unknown[] = [record.id]; + const keys = ['id']; + + record.eachAttribute((key) => { + keys.push(key); + keywords.push(record[key as keyof T]); + }); + + return A(keywords); + } + + /** + Returns the values of filters defined by `getFilters` + These reflect the state of the record + + @method getRecordFilterValues + @private + @param {Model} record + @return {Object} The record state filter values + */ + getRecordFilterValues(record: Model) { + return { + isNew: record.isNew, + isModified: record.hasDirtyAttributes && !record.isNew, + isClean: !record.hasDirtyAttributes, + }; + } + + /** + Returns a color that represents the record's state + Possible colors: black, blue, green + + @method getRecordColor + @private + @param {Model} record + @return {String} The record color + */ + getRecordColor(record: Model) { + let color = 'black'; + if (record.isNew) { + color = 'green'; + } else if (record.hasDirtyAttributes) { + color = 'blue'; + } + return color as RecordColor; + } + + /** + Observes all relevant properties and re-sends the wrapped record + when a change occurs + + @method observeRecord + @private + @param {Model} record + @param {Function} recordUpdated Callback used to notify changes + @return {Function} The function to call to remove all observers + */ + observeRecord(record: Model, recordUpdated: (record: WrappedRecord) => void) { + const releaseMethods: Array<() => void> = []; + const keysToObserve = ['id', 'isNew', 'hasDirtyAttributes']; + + record.eachAttribute((key: string) => keysToObserve.push(key)); + + keysToObserve.forEach((key) => { + const handler = () => { + recordUpdated(this.wrapRecord(record)); + }; + addObserver(record, key, handler); + releaseMethods.push(function () { + removeObserver(record, key, handler); + }); + }); + + const release = function () { + releaseMethods.forEach((fn) => fn()); + }; + + return release; + } +} + +export default macroCondition( + getGlobalConfig<{ WarpDrive: { includeDataAdapter: boolean } }>().WarpDrive.includeDataAdapter +) + ? InspectorDataAdapter + : null; diff --git a/packages/debug/src/index.ts b/packages/debug/src/index.ts new file mode 100644 index 00000000000..9f1e80882f6 --- /dev/null +++ b/packages/debug/src/index.ts @@ -0,0 +1 @@ +export { default } from './data-adapter'; diff --git a/packages/debug/tsconfig.json b/packages/debug/tsconfig.json new file mode 100644 index 00000000000..c1ba91effba --- /dev/null +++ b/packages/debug/tsconfig.json @@ -0,0 +1,76 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "experimentalDecorators": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../legacy-compat/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/model": ["../model/unstable-preview-types"], + "@ember-data/model/*": ["../model/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../store" + }, + { + "path": "../model" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + }, + { + "path": "../legacy-compat" + }, + { + "path": "../request-utils" + } + ] +} diff --git a/packages/debug/vite.config.mjs b/packages/debug/vite.config.mjs new file mode 100644 index 00000000000..1546a794dde --- /dev/null +++ b/packages/debug/vite.config.mjs @@ -0,0 +1,19 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember-data/debug/data-adapter', + '@ember/debug/data-adapter', + '@ember/service', + '@ember/object/observers', + '@ember/array', +]; + +export const entryPoints = ['./src/index.ts', './src/data-adapter.ts', './src/_app_/data-adapter.js']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/diagnostic/CHANGELOG.md b/packages/diagnostic/CHANGELOG.md new file mode 100644 index 00000000000..45ca3d7185e --- /dev/null +++ b/packages/diagnostic/CHANGELOG.md @@ -0,0 +1,70 @@ +# @warp-drive/diagnostic Changelog + +## v0.0.0-alpha.71 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) + +#### :bug: Bug Fix + +* [#9383](https://github.com/emberjs/data/pull/9383) fix: ensure cache-handler clones full errors ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v0.0.0-alpha.9 (2024-02-24) + +#### :memo: Documentation + +* [#9070](https://github.com/emberjs/data/pull/9070) docs: fix note notation to make use of github formatting ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-eslint config ([@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)) +* [#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)) +* [#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)) +* [#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)) +* [#8906](https://github.com/emberjs/data/pull/8906) feat: expand mock-server capabilities, add to main tests ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/diagnostic/LICENSE.md b/packages/diagnostic/LICENSE.md new file mode 100644 index 00000000000..b0ca693ab2d --- /dev/null +++ b/packages/diagnostic/LICENSE.md @@ -0,0 +1,12 @@ +The MIT License (MIT) + +Copyright (C) 2023 EmberData and WarpDrive contributors +Copyright (C) 2017-2022 Ember.js contributors +Portions Copyright (C) 2011-2017 Tilde, Inc. and contributors. +Portions Copyright (C) 2011 LivingSocial Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/diagnostic/NCC-1701-a-blue.svg b/packages/diagnostic/NCC-1701-a-blue.svg new file mode 100644 index 00000000000..3b46f232c1a --- /dev/null +++ b/packages/diagnostic/NCC-1701-a-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/diagnostic/NCC-1701-a.svg b/packages/diagnostic/NCC-1701-a.svg new file mode 100644 index 00000000000..8ee688dcf30 --- /dev/null +++ b/packages/diagnostic/NCC-1701-a.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/diagnostic/README.md b/packages/diagnostic/README.md new file mode 100644 index 00000000000..ae7773a9fc0 --- /dev/null +++ b/packages/diagnostic/README.md @@ -0,0 +1,539 @@ +

+ + +

+ +

⚡️ A Lightweight Modern Test Runner

+

QUnit Compatible (mostly! 🙈)

+ +## Installation + +```cli +pnpm install @warp-drive/diagnostic +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/diagnostic/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40warp-drive/diagnostic/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40warp-drive/diagnostic/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40warp-drive/diagnostic/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40warp-drive/diagnostic/lts-4-12?label=%40lts-4-12&color=bbbbbb) + +--- + +## About + +**@warp-drive/** *diagnostic* is a ground-up revisiting of the APIs [QUnit](https://qunitjs.com/) popularized and [Ember](https://github.com/emberjs/ember-qunit) polished. + +- 💜 Fully Typed +- :electron: Universal +- ⚡️ Fast +- ✅ Easy to use + +**@warp-drive/** *diagnostic* is ***also*** a test launcher/runner inspired by the likes of [Testem](https://github.com/testem/testem), [ember exam](https://github.com/ember-cli/ember-exam) and the [ember test](https://cli.emberjs.com/release/basic-use/cli-commands/#testingyourapp) command. It is similarly flexible, but faster and more lightweight while somehow bringing a more robust feature set to the table. + +- 🚀 Easy Browser Setup Included +- :octocat: Runs without fuss on Github Actions +- 📦 Out of the box randomization, parallelization, load balancing, and more. + +But don't worry, if you're not ready to leave your existing stack the launcher/runner portion is optional. Out of the box, it comes ready with a [Testem](https://github.com/testem/testem) integration, +or you can add your own. + +## Quickstart + +- [Writing Tests](#writing-tests) +- [Running Tests](#running-tests) +- [Using the DOM Reporter](#using-the-domreporter) +- [Concurrency](#concurrency) +- [Using The Launcher](#using-the-launcher) +- [Adding A Sidecar](#adding-a-sidecar) +- [🔜 Parallelism](#parallelism) +- [🔜 Randomization](#randomization) +- [Why Is It Fast?](#why-is-it-fast) +- [Migration From QUnit](#migration-from-qunit) +- [Using with Ember](#using-with-ember) + +--- + +### Writing Tests + +```ts +import { module, test } from '@warp-drive/diagnostic'; + +module('My Module', function(hooks) { + hooks.beforeEach(async function() { + // do setup + }); + + test('It Works!', async function(assert) { + assert.ok('We are up and running'); + }); +}); +``` + +Tests and hooks may be async or sync. + +The `this` context and `assert` instance passed to a `beforeEach` or `afterEach` hook is the same as will be used for the given test but is not shared across tests. + +This makes `this` a convenient pattern for accessing or stashing state during setup/teardown in a manner that is safe for *test concurrency*. + +Global and module level state that is not safely shared between multiple tests potentially running simultaneously should be avoided. + +When augmenting `this`, import `TestContext`. + +```ts +import { type TestContext } from '@warp-drive/diagnostic'; + +interface ModuleContext extends TestContext { + some: 'state'; +} + +module('My Module', function(hooks) { + hooks.beforeEach(async function(this: ModuleContext) { + this.some = 'state'; + }); + + test('It Works!', async function(this: ModuleContext, assert) { + assert.equal(this.some, 'state', 'We are up and running'); + }); +}); +``` + +Alternatively, key some state to a WeakMap and avoid the +type gymnastics. + +```ts +interface ModuleState { + some: 'state'; +} +const STATES = new WeakMap(); + +export function setState(key: object, state: ModuleState) { + STATES.set(key, state); +} + +export function getState(key: object) { + const state = STATES.get(key); + if (!state) { + throw new Error(`Failed to setup state`); + } + return state; +} +``` + +Now all we need to do is use the `this` we already have! + +```ts +import { setState, getState } from './helpers'; + +module('My Module', function(hooks) { + hooks.beforeEach(async function() { + setState(this, { some: 'state' }); + }); + + test('It Works!', async function(assert) { + const state = getState(this); + assert.equal(state.some, 'state', 'We are up and running'); + }); +}); +``` + +--- + +### Running Tests + +> **Note** +> This section is about how to setup your tests to run once launched. To learn about launching tests, read [Using The Launcher](#using-the-launcher) + +> **Warning** +> This section is nuanced, read carefully! + + +To run your tests, import and run `start`. + +```ts +import { start } from '@warp-drive/diagnostic'; + +start(); +``` + +Start will immediately begin running any tests it knows about, +so when you call start matters. + +For instance, if your tests require DOM to be setup, making sure `start` is called only once DOM exists is important. + +If there are global hooks that need configured, that configuration needs to happen *before* you call `start`. Similar with any reporters, `registerReporter` must be called first. + +```ts +import { registerReporter, setupGlobalHooks, start } from '@warp-drive/diagnostic'; +import CustomReporter from './my-custom-reporter'; + +setupGlobalHooks((hooks) => { + hooks.beforeEach(() => { + // .. some setup + }); + hooks.afterEach(() => { + // .. some teardown + }); +}); + +registerReporter(new CustomReporter()); + +start(); +``` + +--- + +### Using the DOMReporter + +For convenience, a `DOMReporter` is provided. When using the `DOMReporter` it expects to be given an element to render the report into. + +```ts +import { registerReporter, start } from '@warp-drive/diagnostic'; +import { DOMReporter } from '@warp-drive/diagnostic/reporters/dom'; + +const container = document.getElementById('warp-drive__diagnostic'); +registerReporter(new DOMReporter(container)); + +start(); +``` + +When using this reporter you will likely want to include the `css` for it, which can be imported from `@warp-drive/diagnostic/dist/styles/dom-reporter.css` + +The specific container element `id` of `warp-drive__diagnostic` only matters if using the provided dom-reporter CSS, custom CSS may be used. + +For convenience, the above code can be condensed by using the DOM `runner`. + +```ts +import { start } from '@warp-drive/diagnostic/runners/dom'; + +start(); +``` + +--- + +### Concurrency + +By default, diagnostic will only run tests one at a time, waiting for all `beforeEach` +and `afterEach` hooks to be called for a test before moving on to the next. + +This is exactly as QUnit would have run the tests. For most this linear mode is +likely a requirement due to state having been stored in module scope or global scope. + +But if you are starting fresh, or have a test suite and program that is very well encapsulated, you may benefit from using test concurrency. + +Emphasis on *may* because concurrency will only help if there is significany empty time +during each test due to things such as `requestAnimationFrame`, `setTimeout` or a +`fetch` request. + +Concurrency is activated by providing a concurrency option in your test suite config. The option should be a positive integer +greater than `1` for it to have any effect. + +```ts +import { configure, start } from '@warp-drive/diagnostic'; + +configure({ + concurrency: 10 +}); + +start(); +``` + +--- + +## Using The Launcher + +#### Quick Setup + +> Skip to [Advanced](#advanced-setup) + +First, we need to add a configuration file for the launcher to our project. + +If our build assets are located in `/dist-test/*` and the entry point for tests is `dist-test/tests/index.html`, then the default configuration will get us setup with no further effort. + +*\/diagnostic.js* +```ts +import launch from '@warp-drive/diagnostic/server/default-setup.js'; + +await launch(); +``` + +Next, adjust the configuration for `start` to tell the runner to emit test information to the diagnostic server. + +```diff +start({ + groupLogs: false, + instrument: true, + hideReport: false, ++ useDiagnostic: true, +}); +``` + +Next, we will want to install `bun`. (We intend to pre-bundle the runner as an executable in the near future, but until then this is required). + +For github-actions, [use the official bun action](https://github.com/oven-sh/setup-bun#readme) + +```yml +- uses: oven-sh/setup-bun@v1 + with: + bun-version: latest +``` + +Finally, give your tests a run to make sure they still work as expected. + +```cli +bun ./diagnostic.js +``` + +And update any necessary scripts in `package.json` + +```diff +{ + "scripts": { + "build" "ember build", +- "test": "ember test" ++ "test": "bun run build && bun ./diagnostic.js" + } +} +``` + +✅ That's all! You're ready to test! 💜 + +--- + +#### Advanced Setup + +--- + +### Adding A Sidecar + +Diagnostic's launcher supports running additional services alongside your test suite +when they are necessary for your tests to run correctly. For instance, you may want +to start a local API instance, http mock service, or a build process. + +#### Use with @warp-drive/holodeck + +@warp-drive/holodeck is an http mock service for test suites. We can start and stop +the holodeck server along side our test server with an easy integration. + +```ts + +``` + +--- + +### Parallelism + +[Coming Soon] + +--- + +### Randomization + +[Coming Soon] + +--- + +### Why Is It Fast? + +There's a number of micro-optimizations, but the primary difference is in "yielding". + +`QUnit` and `ember-qunit` both schedule async checks using `setTimeout`. Even if no work needs to happen and the thread is free, `setTimeout` will delay `~4.5ms` before executing its callback. + +When you delay in this manner multiple times per test, and have lots of tests, things add up. + +In our experience working on EmberData, most of our tests, even our more complicated ones, had +completion times in the `4-30ms` range, the duration of which was dominated by free-time spent +waiting for `setTimeout` callbacks. We did some math and realized that most of our tests run in +less than `0.5ms`, and even our average was `<4ms`, smaller than the time for even a single `setTimeout` +callback. + +`@warp-drive/diagnostic` runs tests as microtasks. Yielding out of the microtask queue only occurs if +the test itself needs to do so. + +> **Note** +> soon we will elect to periodically yield just to allow the DOMReporter to show results, currently its so fast though that the tests are done before you'd care. + +Next, diagnostic, uses several singleton patterns internally to keep allocations to a minimum while +running tests. + +By not waiting for DOMComplete and by being more intelligent about yielding, we start running tests +sooner. In most situations this means test runs start 100-200ms quicker. + +We further noticed that the qunit DOM Reporter was its own bottleneck for both memory and compute time. For our version we made a few tweaks to reduce this cost, which should especially help test suites with thousands or tens of thousands of tests. + +Lastly, we noticed that the serialization and storage of objects being reported had a high cost. +This was a problem shared between the launcher (Testem) and what QUnit was providing to it. For this, +we opted to reduce the amount of information shared to Testem by default to the bare minimum, but with a fast `debug` toggle to switch into the more verbose mode. + +--- + +### Migration from QUnit + +1. Replace `qunit` with `@warp-drive/diagnostic` + +```diff +index 2fbga6a55..c9537dd37 100644 +--- a/package.json ++++ b/package.json +@@ -23,5 +23,5 @@ +- "qunit": "2.20.0", ++ "@warp-drive/diagnostic": "latest", +``` + +2. Update imports from `qunit` to `@warp-drive/diagnostic` + +```diff +--- a/tests/example.ts ++++ b/tests/example.ts +@@ -1,0 +1,0 @@ +- import { module, test } from 'qunit'; ++ import { module, test } from '@warp-drive/diagnostic'; +``` + + +3. Use `equal` and `notEqual` + +Diagnostic has no loose comparison mode. So instead of `strictEqual` and `notStrictEqual` we can just use `equal` and `notEqual` which are already strict. + +4. Update module hooks + +`beforeEach` and `afterEach` are unchanged. +`before` and `after` become `beforeModule` and `afterModule`. + +```diff +module('My Module', function(hooks) { +- hooks.before(function(assert) { ++ hooks.beforeModule(function(assert) { + // ... + }); + +- hooks.after(function(assert) { ++ hooks.afterModule(function(assert) { + // ... + }); +}); +``` + +5. Update global hooks + +`QUnit.begin` and `QUnit.done` become `onSuiteStart` and `onSuiteFinish` respectively. + +`QUnit.hooks` becomes `setupGlobalHooks`. + +```diff ++ import { setupGlobalHooks } from '@warp-drive/diagnostic'; + +- QUnit.begin(function() {}); +- QUnit.done(function() {}); +- QUnit.hooks.beforeEach(function() {}); ++ setupGlobalHooks(function(hooks) { ++ hooks.onSuiteStart(function() {}); ++ hooks.onSuiteFinish(function() {}); ++ hooks.beforeEach(function() {}); ++ }); +``` + +--- + +### Using With Ember + +1. Add the following peer-deps to your app: + +```diff ++ "@ember/test-helpers": "^3.3.0 || ^4.0.4", ++ "ember-cli-test-loader": ">= 3.1.0", ++ "@embroider/addon-shim": ">= 1.8.6" +``` + +2. Configure for ember in `test-helper.js` + +```ts +import { configure } from '@warp-drive/diagnostic/ember'; + +configure(); +``` + +3. Use setup helpers + +```ts +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('My Module', function (hooks) { + setupTest(hooks); +}); +``` + +--- + +### ♥️ Credits + +
+ Brought to you with ♥️ love by 🐹 Ember + + +
diff --git a/packages/diagnostic/addon-main.cjs b/packages/diagnostic/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/diagnostic/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/diagnostic/babel.config.mjs b/packages/diagnostic/babel.config.mjs new file mode 100644 index 00000000000..1cefd69a479 --- /dev/null +++ b/packages/diagnostic/babel.config.mjs @@ -0,0 +1,8 @@ +export default { + plugins: [ + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/unpublished-test-infra/tests/unit/.gitkeep b/packages/diagnostic/bin/.gitkeep similarity index 100% rename from packages/unpublished-test-infra/tests/unit/.gitkeep rename to packages/diagnostic/bin/.gitkeep diff --git a/packages/diagnostic/eslint.config.mjs b/packages/diagnostic/eslint.config.mjs new file mode 100644 index 00000000000..4debba02d3d --- /dev/null +++ b/packages/diagnostic/eslint.config.mjs @@ -0,0 +1,31 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: ['@ember/test-helpers', '@glimmer/manager', '@ember/runloop'], + rules: { + 'no-console': 'off', + }, + }), + + // node (module) ================ + node.esm({ + files: ['server/**/*.{js,ts}'], + globals: { Bun: true }, + rules: { + 'no-console': 'off', + }, + }), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/diagnostic/package.json b/packages/diagnostic/package.json new file mode 100644 index 00000000000..02964f7cea4 --- /dev/null +++ b/packages/diagnostic/package.json @@ -0,0 +1,136 @@ +{ + "name": "@warp-drive/diagnostic", + "version": "4.12.8", + "private": true, + "description": "⚡️ A Lightweight Modern Test Runner", + "keywords": [ + "test", + "assert", + "testrunner", + "tap", + "reporter", + "junit", + "qunit", + "vitest", + "jest", + "mocha", + "chai", + "ember-addon" + ], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/diagnostic" + }, + "license": "MIT", + "author": "Chris Thoburn ", + "files": [ + "unstable-preview-types", + "addon-main.cjs", + "dist/", + "server/", + "README.md", + "LICENSE.md", + "NCC-1701-a.svg", + "NCC-1701-a-blue.svg" + ], + "exports": { + ".": { + "node": "./server/index.js", + "bun": "./server/index.js", + "deno": "./server/index.js", + "browser": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "import": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "default": "./server/index.js" + }, + "./*.css": { + "default": "./dist/*.css" + }, + "./server/*": { + "node": "./server/*.js", + "bun": "./server/*.js", + "deno": "./server/*.js" + }, + "./ember": { + "types": "./unstable-preview-types/ember.d.ts", + "default": "./dist/ember.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:tests": "rm -rf dist-test && cp -R test dist-test && mkdir -p dist-test/@warp-drive && cp -R dist dist-test/@warp-drive/diagnostic", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember/test-helpers": "4.0.4", + "ember-cli-test-loader": ">= 3.1.0" + }, + "peerDependenciesMeta": { + "ember-source": { + "optional": true + }, + "@ember/test-helpers": { + "optional": true + }, + "ember-cli-test-loader": { + "optional": true + } + }, + "dependencies": { + "chalk": "^5.3.0", + "debug": "^4.3.7", + "ember-cli-htmlbars": "^6.3.0", + "tmp": "^0.2.3", + "@warp-drive/build-config": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@babel/runtime": "^7.24.5", + "@warp-drive/internal-config": "workspace:*", + "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", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2, + "preventDownleveling": true + }, + "ember": { + "edition": "octane" + }, + "dependenciesMeta": { + "@warp-drive/build-config": { + "injected": true + } + } +} diff --git a/packages/diagnostic/server/browsers/index.js b/packages/diagnostic/server/browsers/index.js new file mode 100644 index 00000000000..83a34b7442f --- /dev/null +++ b/packages/diagnostic/server/browsers/index.js @@ -0,0 +1,312 @@ +import os from 'os'; +import path from 'path'; +import tmp from 'tmp'; + +import { debug } from '../utils/debug.js'; +import { isWin, platformName } from '../utils/platform.js'; + +export function getHomeDir() { + return process.env.HOME || process.env.USERPROFILE; +} + +function chromeWinPaths(name) { + const homeDir = getHomeDir(); + return [ + homeDir + '\\Local Settings\\Application Data\\Google\\' + name + '\\Application\\chrome.exe', + homeDir + '\\AppData\\Local\\Google\\' + name + '\\Application\\chrome.exe', + 'C:\\Program Files\\Google\\' + name + '\\Application\\Chrome.exe', + 'C:\\Program Files (x86)\\Google\\' + name + '\\Application\\Chrome.exe', + ]; +} + +function chromeDarwinPaths(name) { + const homeDir = getHomeDir(); + return [ + homeDir + '/Applications/' + name + '.app/Contents/MacOS/' + name, + '/Applications/' + name + '.app/Contents/MacOS/' + name, + ]; +} + +const ChromePaths = { + win: chromeWinPaths, + darwin: chromeDarwinPaths, +}; + +const ChromeTags = { + win: { + stable: 'Chrome', + beta: 'Chrome Beta', + canary: 'Chrome SxS', + }, + darwin: { + stable: 'Google Chrome', + beta: 'Google Chrome Beta', + canary: 'Google Chrome Canary', + }, +}; + +const ChromeExeNames = { + stable: ['google-chrome-stable', 'google-chrome', 'chrome'], + beta: ['google-chrome-beta'], + canary: ['google-chrome-unstable'], +}; + +async function executableExists(exe) { + const cmd = isWin() ? 'where' : 'which'; + const result = Bun.spawnSync([cmd, exe], { + stdout: 'inherit', + }); + + return result.success; +} + +async function isInstalled(browser) { + const result = await checkBrowser(browser.possiblePath, fileExists); + if (result) { + return result; + } + + return checkBrowser(browser.possibleExe, function (exe) { + return executableExists(exe); + }); +} + +async function fileExists(file) { + const pointer = Bun.file(file); + return pointer.exists(); +} + +async function checkBrowser(lookups, method) { + if (!lookups) { + return false; + } + + if (Array.isArray(lookups)) { + for (const option of lookups) { + const result = await method(option); + if (result) { + return option; + } + } + return false; + } + + if (await method(lookups)) { + return lookups; + } +} + +async function getChrome(browser, tag) { + const platform = platformName(); + const pathName = ChromeTags[platform]?.[tag]; + const paths = ChromePaths[platform]?.(pathName) ?? []; + + const lookupInfo = { + name: browser.toLowerCase(), + possiblePath: paths, + possibleExe: ChromeExeNames[tag], + }; + + const result = await isInstalled(lookupInfo); + if (!result) { + throw new Error( + `Could not find ${ + lookupInfo.name + } on your system (${platform}).\n\n\tChecked Paths:\n\t\t${lookupInfo.possiblePath.join( + '\n\t\t' + )}\n\tChecked Executable Names:\n\t\t${lookupInfo.possibleExe.join('\n\t\t')}` + ); + } + + debug(`Found ${lookupInfo.name} executable ${result}`); + + return result; +} + +export async function getBrowser(browser) { + const name = browser.toLowerCase(); + if (name === 'chrome') { + return getChrome(name, 'stable'); + } + if (name === 'chrome-beta') { + return getChrome(name, 'beta'); + } + if (name === 'chrome-canary') { + return getChrome(name, 'canary'); + } + + throw new Error(`@warp-drive/diagnostic has no launch information for ${browser}`); +} + +const TMP_DIRS = new Map(); + +export function getTmpDir(browser) { + if (TMP_DIRS.has(browser)) { + return TMP_DIRS.get(browser).name; + } + + const userDataDir = os.tmpdir(); + const tmpPath = path.join(userDataDir, 'testem-' + browser.replace(' ', '_')); + + const tmpDir = tmp.dirSync({ + template: `${tmpPath}-XXXXXX`, + unsafeCleanup: true, + }); + + TMP_DIRS.set(browser, tmpDir); + return tmpDir.name; +} + +export function recommendedArgs(browser, options = {}) { + if (!browser || browser.toLowerCase() !== 'chrome') { + return []; + } + const DEBUG = options.debug || debug.enabled; + const DEBUG_MEMORY = options.memory || process.env.DEBUG_MEMORY; + const SERVE = 'serve' in options ? options.serve : false; + const HEADLESS = 'headless' in options ? options.headless : !SERVE; + const useExisting = 'useExisting' in options ? options.useExisting : false; + const noLaunch = 'noLaunch' in options ? options.noLaunch : false; + + if (noLaunch) { + return []; + } + + if (useExisting) { + return ['--incognito']; + } + + // See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md + // For more details on these flags + return [ + '--user-data-dir=' + getTmpDir(browser), + HEADLESS ? '--headless=new' : false, + '--no-sandbox', + // these prevent user account + // and extensions from mucking with things + '--incognito', + '--bwsi', + '--enable-automation', + + // potentially needed to enable multi-tab + '--allow-http-background-page', + // '--silent-debugger-extension-api', + '--disable-throttle-non-visible-cross-origin-iframes', + // '--memory-saver-multi-state-mode=discarded', + // '--disable-battery-saver-mode', + // '--disable-memory-saver-mode', + // '--enable-background-thread-pool', + // '--disable-background-media-suspend', + // '--disable-tab-discarding', + // '--disable-aggressive-tab-discard', + // disabled because already enabled elsewhere + // '--disable-backgrounding-occluded-windows', + + // Enable Debugging Output + DEBUG ? '--enable-logging=stderr' : '--disable-logging', + DEBUG ? '--v=2' : false, + + // when debugging memory usage this gives us better data + DEBUG_MEMORY ? '--enable-precise-memory-info' : false, + DEBUG_MEMORY ? '--js-flags="--allow-natives-syntax --expose-gc"' : false, + + // Disable Browser Features we don't want + // ===================================== + '--ash-no-nudges', + '--autoplay-policy=user-gesture-required', + '--disable-add-to-shelf', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-default-apps', + '--disable-desktop-notifications', + '--disable-popup-blocking', + '--disable-domain-reliability', + '--disable-extensions', + '--disable-infobars', + '--disable-notifications', + '--disable-search-engine-choice-screen', + '--disable-setuid-sandbox', + '--disable-site-isolation-trials', + '--disable-sync', + '--force-color-profile=srgb', + '--force-device-scale-factor=1', + // This can cause a test to flake + // '--hide-scrollbars', + '--ignore-certificate-errors', + '--disable-proxy-certificate-handler', + // useful in some situations when you trust that + // your tests won't call out to the internet + // '--disable-content-security-policy', + '--mute-audio', + '--no-default-browser-check', + '--no-first-run', + '--test-type', + + // disable specific features + // ===================================== + `--disable-features=${[ + 'ChromeEOLPowerSaveMode', + 'AutofillServerCommunication', + 'AvoidUnnecessaryBeforeUnloadCheckSync', + 'BackForwardCache', + 'BlinkGenPropertyTrees', + 'CalculateNativeWinOcclusion', + 'CertificateTransparencyComponentUpdater', + 'DialMediaRouteProvider', + // 'HeavyAdPrivacyMitigation', + 'InterestFeedContentSuggestions', + 'IsolateOrigins', + 'LazyFrameLoading', + 'MediaRouter', + 'OptimizationHints', + // 'ScriptStreaming', + 'Translate', + ] + .filter(Boolean) + .join(',')}`, + + // Adjust Task Throttling + // ===================================== + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-renderer-backgrounding', + + // Disable Background Networking + // ===================================== + '--disable-background-networking', + DEBUG ? false : '--disable-breakpad', + '--disable-component-update', + '--disable-domain-reliability', + '--no-pings', + + // On Ubuntu this dev-shm-usage speeds you up on bigger machines + // and slows you down on smaller. If you are on a larger CI box + // you should consider re-enabling this. + // off because ubuntu vms currently seem to crash without this + // due to missing drivers + // '--disable-dev-shm-usage', + + // Potentially no longer needed settings + // ===================================== + '--disable-gpu', + '--disable-3d-apis', + '--disable-software-rasterizer', + '--disable-webgl', + // disable-web-security seems to cause browser not able to connect issues + // '--disable-web-security', + '--disable-remote-fonts', + '--blink-settings=imagesEnabled=false', + + // ubuntu-16-core seems to be unhappy with this being set to a non-zero port + // throws: ERROR:socket_posix.cc(147)] bind() failed: Address already in use (98) + options.useEventSimulation ? '--remote-debugging-port=0' : false, + options.useEventSimulation ? '--remote-debugging-address=0.0.0.0' : false, + '--window-size=1440,900', + // no-proxy seems to cause browser not able to connect issues + // '--no-proxy-server', + // '--proxy-bypass-list=*', + // "--proxy-server='direct://'", + ].filter(Boolean); +} diff --git a/packages/diagnostic/server/bun/fetch.js b/packages/diagnostic/server/bun/fetch.js new file mode 100644 index 00000000000..c8421eb4dd7 --- /dev/null +++ b/packages/diagnostic/server/bun/fetch.js @@ -0,0 +1,73 @@ +/* eslint-disable n/no-unsupported-features/node-builtins */ +import chalk from 'chalk'; +import path from 'path'; + +import { INDEX_PATHS } from '../utils/const.js'; +import { debug, info } from '../utils/debug.js'; + +/** @type {import('bun-types')} */ + +export function handleBunFetch(config, state, req, server) { + const url = new URL(req.url); + const protocol = url.protocol; + + if (protocol === 'ws:' || protocol === 'wss:') { + debug(`Upgrading websocket connection`); + server.upgrade(req); + return; + } + + const bId = url.searchParams.get('b') ?? null; + const wId = url.searchParams.get('w') ?? null; + info(`[${chalk.cyan(req.method)}] ${url.pathname}`); + + if (config.parallel > 1 && url.pathname === '/parallel-launcher') { + debug(`Serving parallel launcher`); + const dir = import.meta.dir; + const launcher = path.join(dir, 'launcher.html'); + return new Response(Bun.file(launcher)); + } + + if (INDEX_PATHS.includes(url.pathname)) { + if (bId && wId) { + // serve test index.html + if (config.entry.indexOf('?')) { + config._realEntry = config.entry.substr(0, config.entry.indexOf('?')); + } + debug(`Serving entry ${config._realEntry} for browser ${bId} window ${wId}`); + return new Response(Bun.file(config._realEntry)); + } + const _bId = bId ?? state.lastBowserId ?? state.browserId; + const _wId = wId ?? state.lastWindowId ?? state.windowId; + debug(`Redirecting to ${config.entry} for browser ${_bId} window ${_wId}`); + // redirect to index.html + return Response.redirect(`${protocol}://${state.hostname}:${state.port}?b=${_bId}&w=${_wId}`, { status: 302 }); + } else { + const pathParts = url.pathname.split('/'); + + if (pathParts.at(-1) === '') pathParts.pop(); + if (pathParts[0] === '') pathParts.shift(); + + if (pathParts[0] === 'ws') { + debug(`Upgrading websocket connection`); + server.upgrade(req); + return; + } + + const route = pathParts.join('/'); + if (route === 'favicon.ico') { + return new Response('Not Found', { status: 404 }); + } + + // serve test assets + debug(`Serving asset ${route} for browser ${bId} window ${wId}`); + const asset = Bun.file(path.join(process.cwd(), config.assets, route)); + + return asset.exists().then((exists) => { + if (!exists) { + return new Response('Not Found', { status: 404 }); + } + return new Response(asset); + }); + } +} diff --git a/packages/diagnostic/server/bun/launch-browser.js b/packages/diagnostic/server/bun/launch-browser.js new file mode 100644 index 00000000000..c3088411f3d --- /dev/null +++ b/packages/diagnostic/server/bun/launch-browser.js @@ -0,0 +1,51 @@ +import chalk from 'chalk'; + +import { info, print } from '../utils/debug.js'; + +/** @type {import('bun-types')} */ + +export async function launchBrowsers(config, state) { + const launchers = Object.keys(config.launchers ?? {}); + if (launchers.length === 0) { + throw new Error(`No launchers configured`); + } + + const parallel = config.parallel ?? 1; + for (const launcher of launchers) { + if (!config.launchers[launcher].command) { + throw new Error(`Missing command for launcher ${launcher}`); + } + + const args = config.launchers.chrome.args ?? []; + args.unshift(config.launchers.chrome.command); + const bId = state.browserId++; + + if (parallel > 1) { + const pages = []; + for (let i = 0; i < parallel; i++) { + pages.push(`?b=${bId}&w=${state.windowId++}`); + } + + const launcherUrl = `${state.protocol}://${state.hostname}:${state.port}/parallel-launcher?p[]=${pages.join( + '&p[]=' + )}`; + args.push(launcherUrl); + } else { + args.push(`${state.protocol}://${state.hostname}:${state.port}?b=${bId}&w=${state.windowId++}`); + } + + info(`Spawning:\n\t${args.join('\n\t\t')}`); + const browser = Bun.spawn(args, { + env: process.env, + cwd: process.cwd(), + stdout: 'inherit', + stderr: 'inherit', + }); + state.browsers.set(String(bId), { + launcher, + proc: browser, + }); + info(`${launcher} spawned with pid ${browser.pid}`); + print(chalk.magenta(`⚛️ Launched ${launcher}`)); + } +} diff --git a/packages/diagnostic/server/bun/port.js b/packages/diagnostic/server/bun/port.js new file mode 100644 index 00000000000..95a27402636 --- /dev/null +++ b/packages/diagnostic/server/bun/port.js @@ -0,0 +1,28 @@ +import { debug } from '../utils/debug.js'; + +/** @type {import('bun-types')} */ + +export async function checkPort(port) { + debug(`Checking if port ${port} is available`); + try { + const server = await Bun.listen({ + port, + hostname: '0.0.0.0', + exclusive: true, + socket: { + data() { + debug(`Port ${port} received data 🙈`); + }, + }, + }); + debug(`Port ${port} is available, releasing it for server`); + server.stop(true); + return true; + } catch (e) { + debug(`Port ${port} is not available: ${e.message}`); + if (e.code === 'EADDRINUSE') { + return false; + } + throw e; + } +} diff --git a/packages/diagnostic/server/bun/socket-handler.js b/packages/diagnostic/server/bun/socket-handler.js new file mode 100644 index 00000000000..5773ecc3454 --- /dev/null +++ b/packages/diagnostic/server/bun/socket-handler.js @@ -0,0 +1,89 @@ +import chalk from 'chalk'; + +import { debug, info } from '../utils/debug.js'; +import { sinceStart } from '../utils/time.js'; +import { watchAssets } from './watch.js'; + +export function buildHandler(config, state) { + const Connections = new Set(); + if (config.serve && !config.noWatch) { + watchAssets(config.assets, () => { + Connections.forEach((ws) => { + ws.send(JSON.stringify({ name: 'reload' })); + }); + }); + } + + return { + perMessageDeflate: true, + async message(ws, message) { + const msg = JSON.parse(message); + msg.launcher = state.browsers.get(msg.browserId).launcher; + info(`${chalk.green('➡')} [${chalk.cyan(msg.browserId)}/${chalk.cyan(msg.windowId)}] ${chalk.green(msg.name)}`); + + switch (msg.name) { + case 'suite-start': + if (!state.started) { + state.started = true; + config.reporter.onRunStart(msg); + } + config.reporter.onSuiteStart(msg); + break; + case 'test-start': + config.reporter.onTestStart(msg); + break; + case 'test-finish': + config.reporter.onTestFinish(msg); + break; + case 'suite-finish': + config.reporter.onSuiteFinish(msg); + + if (!config.serve) { + ws.send(JSON.stringify({ name: 'close' })); + ws.close(); + } + state.completed++; + debug( + `${chalk.green('✅ [Complete]')} ${chalk.cyan(msg.browserId)}/${chalk.cyan(msg.windowId)} ${chalk.yellow( + '@' + sinceStart() + )}` + ); + if (state.completed === state.expected) { + const exitCode = config.reporter.onRunFinish(msg); + debug(`${chalk.green('✅ [All Complete]')} ${chalk.yellow('@' + sinceStart())}`); + + if (!config.serve) { + state.browsers.forEach((browser) => { + browser.proc.kill(); + browser.proc.unref(); + }); + state.server.stop(); + if (config.cleanup) { + debug(`Running configured cleanup hook`); + await config.cleanup(); + debug(`Configured cleanup hook completed`); + } + // 1. We expect all cleanup to have happened after + // config.cleanup(), so exiting here should be safe. + // 2. We also want to forcibly exit with a success code in this + // case. + // eslint-disable-next-line n/no-process-exit + process.exit(exitCode); + } + } + + break; + } + // console.log(JSON.parse(message)); + }, // a message is received + open(ws) { + Connections.add(ws); + debug(`WebSocket opened`); + }, // a socket is opened + close(ws, code, message) { + Connections.delete(ws); + debug(`WebSocket closed`); + }, // a socket is closed + drain(ws) {}, // the socket is ready to receive more data + }; +} diff --git a/packages/diagnostic/server/bun/watch.js b/packages/diagnostic/server/bun/watch.js new file mode 100644 index 00000000000..e6be7cc4cc0 --- /dev/null +++ b/packages/diagnostic/server/bun/watch.js @@ -0,0 +1,39 @@ +import { watch } from 'fs'; + +export function addCloseHandler(cb) { + let executed = false; + + process.on('SIGINT', () => { + if (executed) return; + executed = true; + cb(); + }); + + process.on('SIGTERM', () => { + if (executed) return; + executed = true; + cb(); + }); + + process.on('SIGQUIT', () => { + if (executed) return; + executed = true; + cb(); + }); + + process.on('exit', () => { + if (executed) return; + executed = true; + cb(); + }); +} + +export function watchAssets(directory, onAssetChange) { + const watcher = watch(directory, { recursive: true }, (event, filename) => { + onAssetChange(event, filename); + }); + + addCloseHandler(() => { + watcher.close(); + }); +} diff --git a/packages/diagnostic/server/default-setup.js b/packages/diagnostic/server/default-setup.js new file mode 100644 index 00000000000..e0ac49ce1f6 --- /dev/null +++ b/packages/diagnostic/server/default-setup.js @@ -0,0 +1,109 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; + +import { getBrowser, recommendedArgs } from './browsers/index.js'; +import launch from './index.js'; +import DefaultReporter from './reporters/default.js'; +import { getFlags } from './utils/get-flags.js'; + +const CI_BROWSER = process.env.CI_BROWSER || 'Chrome'; +const BROWSER_TAG = CI_BROWSER.toLowerCase(); + +const browser = await getBrowser(BROWSER_TAG); + +let TEST_FAILURES; +try { + const filePath = path.join(process.cwd(), './diagnostic-failed-test-log.txt'); + TEST_FAILURES = fs.readFileSync(filePath, { encoding: 'utf-8' }); +} catch { + TEST_FAILURES = false; +} +const FAILURES = TEST_FAILURES ? TEST_FAILURES.trim().split(',') : false; + +// default 13min per-browser test suite run timeout in seconds +const DEFAULT_SUITE_TIMEOUT = 780; +// when using a configured timeout we adjust it down a bit to account for +// to make sure we cleanup before external things cleanup +const SUITE_TIMEOUT_BUFFER = 30; +const SUITE_TIMEOUT = process.env.SUITE_TIMEOUT + ? Number(process.env.SUITE_TIMEOUT) - SUITE_TIMEOUT_BUFFER + : DEFAULT_SUITE_TIMEOUT; + +export default async function launchDefault(overrides = {}) { + const flags = getFlags().filtered; + Object.assign(overrides, flags); + + const RETRY_TESTS = + ('retry' in overrides ? overrides.retry : (process.env.CI ?? process.env.RETRY_TESTS)) && FAILURES.length; + const _parallel = + process.env.DIAGNOSTIC_PARALLEL && !isNaN(Number(process.env.DIAGNOSTIC_PARALLEL)) + ? Number(process.env.DIAGNOSTIC_PARALLEL) + : 1; + const parallel = _parallel > 1 && RETRY_TESTS && FAILURES.length < _parallel * 4 ? 1 : _parallel; + + if (RETRY_TESTS) { + console.log( + chalk.grey( + `⚠️ Retrying ${chalk.bold(chalk.yellow(FAILURES.length))} failed tests: ${chalk.bold( + chalk.white(FAILURES.join(',')) + )}` + ) + ); + } else if (FAILURES.length) { + console.log( + `⚠️ Found ${chalk.bold(chalk.yellow(FAILURES.length))} previously failed tests: ${chalk.bold( + chalk.white(FAILURES.join(',')) + )}. Use RETRY_TESTS=1 or --retry/-r to retry them.` + ); + } + const DEBUG = Boolean(process.env.DEBUG ?? overrides.debug ?? false); + + const TEST_PAGE_FLAGS = [ + process.env.DEBUG_MEMORY ? 'memory=1' : false, + process.env.CI || process.env.DEBUG_MEMORY ? 'hideReport=1' : false, + process.env.DEBUG_PERFORMANCE ? 'performance=1' : false, + DEBUG ? 'debug=1' : false, + RETRY_TESTS ? `testId=${FAILURES.join('&testId=')}` : false, + ].filter(Boolean); + + console.log( + `\n\nLaunching with ${chalk.bold(chalk.cyan(CI_BROWSER))} (worker count ${chalk.bold(chalk.yellow(parallel))})\n\n` + ); + + await launch({ + // flag config + serve: overrides.serve ?? false, + noLaunch: overrides.noLaunch ?? false, + filter: overrides.filter ?? false, + debug: overrides.debug ?? false, + headless: overrides.headless ?? false, + useExisting: overrides.useExisting ?? false, + + entry: overrides.entry ?? `./dist-test/tests/index.html?${TEST_PAGE_FLAGS.join('&')}`, + assets: overrides.assets ?? './dist-test', + parallel: overrides.parallel ?? parallel, + parallelMode: overrides.parallelMode ?? 'window', // 'tab' | 'browser' | 'window' + + reporter: + overrides.reporter ?? + new DefaultReporter({ + mode: process.env.DIAGNOSTIC_REPORTER_MODE || 'dot', // 'dot' | 'compact' | 'verbose' + }), + + suiteTimeout: overrides.suiteTimeout ?? SUITE_TIMEOUT, + browserDisconnectTimeout: overrides.browserDisconnectTimeout ?? 15, + browserStartTimeout: overrides.browserStartTimeout ?? 15, + socketHeartbeatTimeout: overrides.socketHeartbeatTimeout ?? 15, + + setup: overrides.setup ?? (() => {}), + cleanup: overrides.cleanup ?? (() => {}), + + launchers: overrides.launchers ?? { + [BROWSER_TAG]: { + command: browser, + args: recommendedArgs(BROWSER_TAG, overrides), + }, + }, + }); +} diff --git a/packages/diagnostic/server/index.js b/packages/diagnostic/server/index.js new file mode 100644 index 00000000000..b75e3bcd5a4 --- /dev/null +++ b/packages/diagnostic/server/index.js @@ -0,0 +1,90 @@ +import chalk from 'chalk'; + +import { handleBunFetch } from './bun/fetch.js'; +import { launchBrowsers } from './bun/launch-browser.js'; +import { buildHandler } from './bun/socket-handler.js'; +import { debug, error, print } from './utils/debug.js'; +import { getPort } from './utils/port.js'; + +/** @type {import('bun-types')} */ +const isBun = typeof Bun !== 'undefined'; + +export default async function launch(config) { + if (isBun) { + debug(`Bun detected, using Bun.serve()`); + + const { checkPort } = await import('./bun/port.js'); + const hostname = config.hostname ?? 'localhost'; + const protocol = config.protocol ?? 'http'; + const port = await getPort(config, checkPort); + + const serveOptions = { + port, + hostname, + }; + + const state = { + browserId: 42, + lastBowserId: null, + windowId: 0, + lastWindowId: null, + port, + hostname, + protocol, + browsers: new Map(), + completed: 0, + expected: config.parallel ?? 1, + }; + + if (protocol === 'https') { + if (!config.key) throw new Error(`Missing key for https protocol`); + if (!config.cert) throw new Error(`Missing cert for https protocol`); + + serveOptions.tls = { + key: Bun.file(config.key), + cert: Bun.file(config.cert), + }; + } + + try { + state.server = Bun.serve({ + ...serveOptions, + development: false, + exclusive: true, + fetch(req, server) { + return handleBunFetch(config, state, req, server); + }, + websocket: buildHandler(config, state), + }); + print(chalk.magenta(`🚀 Serving on ${chalk.white(protocol + '://' + hostname + ':')}${chalk.magenta(port)}`)); + config.reporter.serverConfig = { + port, + hostname, + protocol, + url: `${protocol}://${hostname}:${port}`, + }; + + if (config.setup) { + debug(`Running configured setup hook`); + await config.setup({ + port, + hostname, + protocol, + }); + debug(`Configured setup hook completed`); + } + + await launchBrowsers(config, state); + } catch (e) { + error(`Error: ${e?.message ?? e}`); + if (config.cleanup) { + debug(`Running configured cleanup hook`); + await config.cleanup(); + debug(`Configured cleanup hook completed`); + } + throw e; + } + } else { + throw new Error(`Diagnostic is not supported in this environment.`); + } +} diff --git a/packages/diagnostic/server/launcher.html b/packages/diagnostic/server/launcher.html new file mode 100644 index 00000000000..cea14159dba --- /dev/null +++ b/packages/diagnostic/server/launcher.html @@ -0,0 +1,39 @@ + + + + @warp-drive/diagnostic Parallel Test Launcher + + + + diff --git a/packages/diagnostic/server/reporters/default.js b/packages/diagnostic/server/reporters/default.js new file mode 100644 index 00000000000..0c980474f95 --- /dev/null +++ b/packages/diagnostic/server/reporters/default.js @@ -0,0 +1,554 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; + +const SLOW_TEST_COUNT = 50; +const DEFAULT_TIMEOUT = 8_000; +const TIMEOUT_BUFFER = 0; +const DEFAULT_TEST_TIMEOUT = 21_000; +const failedTestsFile = path.join(process.cwd(), './diagnostic-failed-test-log.txt'); + +function indent(text, width = 2) { + return text + .split('\n') + .map((line) => { + return new Array(width).join('\t') + line; + }) + .join('\n'); +} + +const HEADER_STR = '==================================================================='; + +export default class CustomDotReporter { + // serverConfig will be injected by the server + constructor(config) { + this.config = config; + + // what format to print + this.isDotFormat = config.mode === 'dot'; + this.isCompactFormat = config.mode === 'compact'; + this.isVerboseFormat = config.mode === 'verbose'; + + this.out = process.stdout; + + // launcher tracking + this.launchers = {}; + this.tabs = new Map(); + this.idsToStartNumber = new Map(); + + // run infos + this.startNumber = 1; + this.startTime = null; + this.realStartTime = null; + this.timeZero = 0; + this.dateTimeZero = Date.now() - performance.now(); + + // results + this.results = []; + this.failedTests = []; + this.globalFailures = []; + this.failedTestIds = new Set(); + this.total = 0; + this.pass = 0; + this.skip = 0; + this.todo = 0; + this.fail = 0; + + // display info + this.shouldPrintHungTests = false; + + // dot display info + this.lineFailures = []; + this.currentLineChars = 0; + this.maxLineChars = 60; + this.totalLines = 0; + } + + clearState() { + this.launchers = {}; + this.tabs.clear(); + this.idsToStartNumber.clear(); + this.results = []; + this.failedTests = []; + this.globalFailures = []; + this.failedTestIds.clear(); + this.total = 0; + this.pass = 0; + this.skip = 0; + this.todo = 0; + this.fail = 0; + this.shouldPrintHungTests = false; + this.lineFailures = []; + this.currentLineChars = 0; + this.totalLines = 0; + } + + write(str) { + this.out.write(str); + } + + // Hooks + // ============== + onRunStart(runInfo) { + this.startTime = performance.now(); + this.realStartTime = runInfo.timestamp; + + const runDelta = this.startTime - this.timeZero; + const elapsed = this.realStartTime - this.dateTimeZero; + + this.write( + `\n\n${HEADER_STR}\n Test Run Initiated\n\tSuite Start: ${chalk.cyan( + new Date(this.realStartTime).toLocaleString('en-US') + )} (elapsed ${chalk.cyan(elapsed.toLocaleString('en-US'))} ms)\n\tReporter Start: ${chalk.cyan( + new Date().toLocaleString('en-US') + )} (elapsed ${chalk.cyan(runDelta.toLocaleString('en-US'))} ms)\n${HEADER_STR}\n\n` + ); + } + + onSuiteStart(suiteInfo) { + this.addLauncher(suiteInfo); + } + + onTestStart(report) { + this.getTab(report).running.set(report.data.testId, report); + report.testNo = this.startNumber++; + report._testStarted = this.now(); + this.idsToStartNumber.set(`${report.browserId}:${report.windowId}:${report.data.testId}`, report.testNo); + this.ensureTimeoutCheck(); + report.launcherDescription = `${report.launcher}:${report.browserId}:${report.windowId}`; + + report.name = `${report.launcherDescription} #${report.testNo} ${chalk.magenta( + '@ ' + (Math.round(report._testStarted / 10) / 100).toLocaleString('en-US') + 's' + )} ${report.data.name}`; + + if (process.env.DISPLAY_TEST_NAMES) { + this.write(`\t\t⏱️ ${chalk.magenta(' Started')}: ${report.name}\n`); + } + } + + onTestFinish(report) { + const tab = this.getTab(report); + const startNoKey = `${report.browserId}:${report.windowId}:${report.data.testId}`; + const startNo = this.idsToStartNumber.get(startNoKey); + + report.testNo = startNo ?? ''; + report.data.runDuration = report.data.runDuration ?? 0; + report.launcherDescription = `${report.launcher}:${report.browserId}:${report.windowId}`; + + if (tab.running.has(report.data.testId)) tab.running.delete(report.data.testId); + + if (this.isCompactFormat) { + this.displayFullResult(report, false); + } else if (this.isDotFormat) { + if (this.results.length === 0) this.displayDotLegend(); + this.displayDotResult(report); + } else if (this.isVerboseFormat) { + this.displayFullResult(report, true); + } else { + throw new Error(`Unknown Reporter Mode ${this.config.mode}. Please use one of 'dot', 'compact', or 'verbose'`); + } + + const { data } = report; + + this.results.push(report); + this.total++; + if (data.skipped) { + this.skip++; + } else if (data.passed && !data.todo) { + this.pass++; + } else if (!data.passed && data.todo) { + this.todo++; + } else { + this.fail++; + } + + if (data.failed && !data.skipped && !data.todo) { + this.lineFailures.push(report); + this.failedTests.push(report); + this.failedTestIds.add(data.testId); + } + } + + onGlobalFailure(report) { + this.globalFailures.push(report); + this.fail++; + } + + onSuiteFinish() {} + + onRunFinish(runReport) { + if (this.failedTests.length) { + this.write( + chalk.red( + `\n\n${this.failedTests.length} Tests Failed. Complete stack traces for failures will print at the end.` + ) + ); + } + this.write(`\n\n`); + + this.reportPendingTests(); + this.reportSlowTests(); + this.reportFailedTests(); + + this.summarizeResults(); + + // Print run duration stats + const { startTime, realStartTime } = this; + const endTime = performance.now(); + const endDate = new Date(); + const fullElapsed = endTime - this.timeZero; + const runElapsed = endTime - startTime; + const realEndTime = runReport.timestamp; + const suiteElapsed = realEndTime - realStartTime; + const realEndDate = new Date(realEndTime); + + this.write( + `\n\n${HEADER_STR}\n Test Run Complete\n\tSuite End: ${chalk.cyan( + realEndDate.toLocaleString('en-US') + )} (elapsed ${chalk.cyan(suiteElapsed.toLocaleString('en-US'))} ms)\n\tReporter End: ${chalk.cyan( + endDate.toLocaleString('en-US') + )} (elapsed ${chalk.cyan(runElapsed.toLocaleString('en-US'))} ms)\n\tRun Duration ${chalk.cyan( + fullElapsed.toLocaleString('en-US') + )} ms\n${HEADER_STR}\n\n` + ); + + this.clearState(); + + return this.failedTests.length ? 1 : 0; + } + + addLauncher(data) { + this.launchers = this.launchers || {}; + this.tabs = this.tabs || new Map(); + + const { launcher, browserId, windowId } = data; + this.launchers[launcher] = this.launchers[launcher] || {}; + const browser = (this.launchers[launcher][browserId] = this.launchers[launcher][browserId] || { + launcher, + id: browserId, + tabs: new Set(), + }); + + const tabId = `${browserId}:${windowId}`; + if (browser.tabs.has(tabId)) { + return; + } + + browser.tabs.add(tabId); + this.tabs.set(tabId, { + running: new Map(), + }); + } + + getTab(test) { + const { windowId, browserId } = test; + const tabId = `${browserId}:${windowId}`; + + return this.tabs.get(tabId); + } + + now() { + return performance.now() - this.startTime; + } + + displayDotLegend() { + this.write('\n\tLegend\n\t========='); + this.write(chalk.green('\n\tPass:\t.')); + this.write(chalk.cyan('\n\tTodo:\tT')); + this.write(chalk.yellow('\n\tSkip:\t*')); + this.write(chalk.bold(chalk.red('\n\tFail:\tF'))); + this.write('\n\n\t'); + } + + displayDotResult(report) { + // complete line + if (this.currentLineChars > this.maxLineChars) { + if (this.shouldPrintHungTests) { + this.shouldPrintHungTests = false; + this.reportHungTests(); + } + + this.totalLines++; + this.currentLineChars = 0; + const lineFailures = this.lineFailures; + this.lineFailures = []; + + if (lineFailures.length) { + this.write('\n\n'); + lineFailures.forEach((failure) => { + this.displayFullResult(failure, false); + }); + } + + if (this.totalLines % 5 === 0) { + this.write(`\n${chalk.magenta((this.totalLines * this.maxLineChars).toLocaleString('en-US'))}⎡\t`); + } else { + this.write('\n\t'); + } + } + + const result = report.data; + if (result.passed && !result.todo) { + this.write(chalk.grey('.')); + } else if (!result.passed && result.todo) { + this.write(chalk.cyan('T')); + } else if (result.skipped) { + this.write(chalk.yellow('*')); + } else { + this.write(chalk.bold(chalk.red('F'))); + } + this.currentLineChars += 1; + } + + displayFullResult(report, verbose) { + const result = report.data; + const name = `${chalk.grey(result.runDuration.toLocaleString('en-US') + 'ms')} ${chalk.white( + '#' + report.testNo + )} ${result.name} ${chalk.grey(report.launcherDescription)}`; + if (result.passed && !result.todo) { + this.write(`\t✅ ${chalk.green('Passed')}: ${name}\n`); + } else if (!result.passed && result.todo) { + this.write(chalk.cyan(`\t🛠️ TODO: ${name}\n`)); + } else if (result.skipped) { + this.write(chalk.yellow(`\t⚠️ Skipped: ${name}\n`)); + } else { + this.write(chalk.red(`\t💥 Failed: ${name}\n`)); + this.write(`\t\topen test locally: ${this.serverConfig.url}?testId=${result.testId}\n`); + + // TODO - print individual failures in verbose mode + } + } + + summarizeResults() { + const lines = [ + 'Result', + '=========', + 'Total ' + this.total, + chalk.green('# pass ' + this.pass), + chalk.yellow('# skip ' + this.skip), + chalk.cyan('# todo ' + this.todo), + chalk.red('# fail ' + this.fail), + ]; + + if (this.pass + this.skipped + this.todo === this.total) { + lines.push(''); + lines.push('# ok'); + } + this.write('\n\n\t'); + this.write(lines.join('\n\t')); + this.write('\n\n'); + } + + // special reporting functionality + // =============================== + + /** + * Periodically checks for hung tests and reports them + */ + ensureTimeoutCheck() { + if (this._timeoutId) { + return; + } + this._timeoutId = setTimeout(() => { + this.shouldPrintHungTests = true; + }, DEFAULT_TEST_TIMEOUT / 3); + } + + reportHungTests() { + let hasRunningTests = false; + this.tabs.forEach((tab) => { + const running = tab.running; + + running.forEach((report) => { + hasRunningTests = true; + const duration = this.now() - report._testStarted; + if (duration > DEFAULT_TEST_TIMEOUT) { + this.write( + chalk.grey( + `\n\n⚠️ ${chalk.yellow('Pending:')} ${chalk.white(report.name)} has been running for ${chalk.yellow( + duration.toLocaleString('en-US') + 'ms' + )}, this is likely a bug.\n` + ) + ); + } + }); + }); + + this._timeoutId = null; + if (hasRunningTests) { + this.ensureTimeoutCheck(); + } + } + + /** + * Same as `reportHungTests` but is for use to report everything + * that is currently running when the test suite completes. + */ + reportPendingTests() { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + this._timeoutId = null; + } + + this.tabs.forEach((tab) => { + const running = tab.running; + let hasFoundPending = false; + + running.forEach((report) => { + if (!hasFoundPending) { + this.write(chalk.red(`\n\nStill Pending Tests:\n\n`)); + hasFoundPending = true; + } + + const duration = this.now() - report._testStarted; + + this.write( + chalk.yellow( + `\t⛔️ Stuck (${chalk.red(duration.toLocaleString('en-US') + ' ms')}): (${ + report.data.testId + }) ${chalk.white(report.name)} ${chalk.grey(report.launcherDescription)}\n` + ) + ); + }); + }); + } + + reportSlowTests() { + const results = this.results; + let totalDuration = 0; + let testsToPrint = SLOW_TEST_COUNT; + results.sort((a, b) => { + return a.runDuration > b.runDuration ? -1 : 1; + }); + + this.write( + `\n\n\t${chalk.yellow( + `${results.length < SLOW_TEST_COUNT ? results.length : SLOW_TEST_COUNT} Longest Running Tests` + )}\n${HEADER_STR}\n` + ); + for (let i = 0; i < results.length; i++) { + const { name, runDuration } = results[i].data; + + if (i < testsToPrint) { + // this test is a known offender + if (runDuration > DEFAULT_TIMEOUT + TIMEOUT_BUFFER) { + this.write(`\n\t${i + 1}.\t[S] ${chalk.yellow(runDuration.toLocaleString('en-US') + 'ms')}\t${name}`); + testsToPrint++; + } else { + this.write(`\n\t${i + 1}.\t${chalk.yellow(runDuration.toLocaleString('en-US') + 'ms')}\t${name}`); + } + } + totalDuration += runDuration; + } + this.write( + chalk.yellow( + `\n\n\tAvg Duration of all ${results.length} tests: ${Math.round(totalDuration / results.length)}ms\n\n` + ) + ); + } + + reportFailedTests() { + this.failedTests.forEach((failure) => { + const result = failure.data; + this.write(chalk.red(`\n\t💥 Failed: ${result.runDuration.toLocaleString('en-US')}ms ${result.name}\n`)); + + result.items.forEach((diagnostic) => { + this.write(`\t\t${diagnostic.passed ? chalk.green('✅ Pass') : chalk.red('💥 Fail')} ${diagnostic.message}\n`); + + if (!diagnostic.passed && 'expected' in diagnostic && 'actual' in diagnostic) { + this.write( + `\n\t\texpected: ${printValue(diagnostic.expected, 3)}\n\t\tactual: ${printValue(diagnostic.actual, 3)}\n` + ); + } + + if (!diagnostic.passed && diagnostic.stack) { + this.write(`\n${indent(diagnostic.stack)}\n`); + } + }); + + this.write('\n\n'); + }); + + if (this.globalFailures.length) { + this.write(chalk.red(`\n\n${this.globalFailures.length} Global Failures\n\n`)); + } + + this.globalFailures.forEach((failure) => { + const result = failure.error; + const label = + result.name && result.message + ? `[${result.name}] ${result.message}` + : result.name || result.message || 'Unknown Error'; + this.write(chalk.red(`\n\t💥 Failed: ${label}\n`)); + + if (result.stack) { + this.write(`\n${indent(result.stack)}\n`); + } + + this.write('\n\n'); + }); + } + + updateFailedTestCache() { + const failedTestIds = [...this.failedTestIds.entries()]; + const allFailuresAccounted = this.globalFailures.length === 0; + const cacheFile = failedTestsFile; + + if (allFailuresAccounted) { + if (failedTestIds.length) { + fs.writeFileSync(cacheFile, failedTestIds.join(','), { encoding: 'utf-8' }); + + this.write( + chalk.yellow( + `\n\nSaved ${chalk.white(failedTestIds.length)} Failed Tests for Retry with IDS ${chalk.white( + failedTestIds.join(',') + )} in ${chalk.grey(cacheFile)}` + ) + ); + + this.write( + `\n\nTo run failed tests locally, ${chalk.cyan('visit')} ${chalk.white( + `${this.serverConfig.url}?${failedTestIds.map((id) => `testId=${id}`).join('&')}` + )}` + ); + } else { + remove(cacheFile); + } + } else { + if (failedTestIds.length) { + this.write( + `\n\nTo run failed tests locally, ${chalk.cyan('visit')} ${chalk.white( + `${this.serverConfig.url}?${failedTestIds.map((id) => `testId=${id}`).join('&')}` + )}` + ); + } + this.write(chalk.red(`\n\n⚠️ Unable to save failed tests for retry, not all failures had test IDs, cleaning up`)); + remove(cacheFile); + } + } +} + +// Instead of completely removing, we replace the contents with an empty string so that CI will still cache it. +// While this shouldn't ever really be necessary it's a bit more correct to make sure that the log gets cleared +// in the cache as well. +function remove(filePath) { + fs.writeFileSync(filePath, '', { encoding: 'utf-8' }); +} + +function printValue(value, tabs = 0) { + if (typeof value === 'string') { + return value; + } else if (typeof value === 'number') { + return value; + } else if (typeof value === 'boolean') { + return String(value); + } else if (value === null) { + return 'null'; + } else if (value === undefined) { + return 'undefined'; + } else if (Array.isArray(value)) { + return indent(`[\n ${value.map((v) => printValue(v, tabs + 1)).join(',\n ')}\n]`, tabs); + } else if (typeof value === 'object') { + return JSON.stringify(value, null, tabs * 4); + } +} diff --git a/packages/diagnostic/server/utils/const.js b/packages/diagnostic/server/utils/const.js new file mode 100644 index 00000000000..591dafed205 --- /dev/null +++ b/packages/diagnostic/server/utils/const.js @@ -0,0 +1,6 @@ +export const DEFAULT_PORT = 7357; +export const DEFAULT_HOST = 'localhost'; +export const DEFAULT_PROTOCOL = 'http'; +export const MAX_PORT_TRIES = 100; + +export const INDEX_PATHS = ['', '/', 'index.html']; diff --git a/packages/diagnostic/server/utils/debug.js b/packages/diagnostic/server/utils/debug.js new file mode 100644 index 00000000000..b56fb819541 --- /dev/null +++ b/packages/diagnostic/server/utils/debug.js @@ -0,0 +1,67 @@ +import _debug from 'debug'; +const _log = _debug('wd:diagnostic'); + +export const DEBUG_LEVEL = parseDebugLevel(process.env.DEBUG_LEVEL); + +function parseDebugLevel(v) { + if (typeof v === 'string' && v && isNaN(Number(v))) { + return getDebugLevel(v); + } else if (typeof v === 'number') { + return v; + } else if (v && !isNaN(Number(v))) { + return Number(v); + } + return 1; +} + +function getDebugLevel(str) { + switch (str.toLowerCase()) { + case 'debug': + return 0; + case 'info': + case 'log': + return 1; + case 'warn': + return 2; + case 'error': + return 3; + default: + return 1; + } +} + +export function print(message) { + if (_log.enabled) { + _log(message); + } else { + console.log(message); + } +} + +export function debug(message) { + if (DEBUG_LEVEL === 0) { + _log(message); + } +} + +export function log(message) { + if (DEBUG_LEVEL <= 1) { + _log(message); + } +} + +export function info(message) { + if (DEBUG_LEVEL <= 1) { + _log(message); + } +} + +export function warn(message) { + if (DEBUG_LEVEL <= 2) { + _log(message); + } +} + +export function error(message) { + _log(message); +} diff --git a/packages/diagnostic/server/utils/get-flags.js b/packages/diagnostic/server/utils/get-flags.js new file mode 100644 index 00000000000..2b76e754020 --- /dev/null +++ b/packages/diagnostic/server/utils/get-flags.js @@ -0,0 +1,68 @@ +export function getFlags() { + const raw = process.argv.slice(2); + for (let i = 0; i < raw.length; i++) { + const rawArg = raw[i]; + if (rawArg.startsWith('--')) { + continue; + } else if (rawArg.startsWith('-')) { + const args = rawArg.slice(1); + if (args.length > 1) { + for (let j = 0; j < args.length; j++) { + raw.push(`-${args[j]}`); + } + } + } + } + const flags = new Set(raw); + const filtered = {}; + + // global flags + const noWatch = flags.has('--no-watch') || flags.has('-w'); + const debug = flags.has('--debug') || flags.has('-d'); + const serve = flags.has('--serve') || flags.has('-s'); + const noLaunch = flags.has('--no-launch') || flags.has('-n'); + const filter = flags.has('--filter') || flags.has('-f'); + const retry = flags.has('--retry') || flags.has('-r'); + const headless = flags.has('--headless') || flags.has('-h'); + const useExisting = flags.has('--use-existing') || flags.has('-e'); + + if (filter) { + filtered['filter'] = true; + } + if (debug) { + filtered['debug'] = true; + } + if (serve) { + filtered['serve'] = true; + } + if (noLaunch) { + filtered['noLaunch'] = true; + } + if (retry) { + filtered['retry'] = true; + } + if (headless) { + filtered['headless'] = true; + } + if (useExisting) { + filtered['useExisting'] = true; + } + if (noWatch) { + filtered['noWatch'] = true; + } + + return { + parsed: { + debug, + serve, + noLaunch, + filter, + retry, + noWatch, + headless, + useExisting, + }, + filtered, + flags, + }; +} diff --git a/packages/diagnostic/server/utils/platform.js b/packages/diagnostic/server/utils/platform.js new file mode 100644 index 00000000000..8380edf1306 --- /dev/null +++ b/packages/diagnostic/server/utils/platform.js @@ -0,0 +1,35 @@ +import os from 'os'; + +function test(platform) { + return /^win/.test(platform); +} + +const currentPlatform = test(os.platform()); + +export function isWin(platform) { + if (platform) { + return test(platform); + } + + return currentPlatform; +} + +export function isMac(platform) { + if (platform) { + return /^darwin/.test(platform); + } + + return /^darwin/.test(os.platform()); +} + +export function isLinux(platform) { + if (platform) { + return /^linux/.test(platform); + } + + return /^linux/.test(os.platform()); +} + +export function platformName() { + return isWin() ? 'win' : os.platform(); +} diff --git a/packages/diagnostic/server/utils/port.js b/packages/diagnostic/server/utils/port.js new file mode 100644 index 00000000000..a5630dd0aef --- /dev/null +++ b/packages/diagnostic/server/utils/port.js @@ -0,0 +1,33 @@ +import { DEFAULT_PORT, MAX_PORT_TRIES } from './const.js'; +import { debug } from './debug.js'; + +async function discoverPort(defaultPort, checkPort) { + debug(`Discovering available port starting from default port of ${defaultPort}`); + let port = defaultPort; + + for (let i = 0; i < MAX_PORT_TRIES; i++) { + if (await checkPort(port)) { + return port; + } + port++; + } + + throw new Error(`Could not find an available port in the range ${defaultPort} to ${port}`); +} + +export async function getPort(config, checkPort) { + if (typeof config.port === 'number') { + if (config.port < 0 || config.port > 65535) { + throw new Error(`Invalid port number: ${config.port}`); + } else if (config.port === 0) { + debug('Port is set to 0, discovering available port'); + return await discoverPort(config.defaultPort || DEFAULT_PORT, checkPort); + } else { + await checkPort(config.port); + return config.port; + } + } else { + debug(`Port is not set, discovering available port`); + return await discoverPort(config.defaultPort || DEFAULT_PORT, checkPort); + } +} diff --git a/packages/diagnostic/server/utils/time.js b/packages/diagnostic/server/utils/time.js new file mode 100644 index 00000000000..cb4b06d6bf9 --- /dev/null +++ b/packages/diagnostic/server/utils/time.js @@ -0,0 +1,16 @@ +export function sinceStart() { + const time = performance.now(); + const seconds = Math.floor(time / 1000); + const minutes = Math.floor(seconds / 60); + const ms = Math.floor(time % 1000); + + if (minutes) { + return `${minutes.toLocaleString('en-US')}m ${seconds % 60}s ${ms.toLocaleString('en-US')}ms`; + } + + if (seconds) { + return `${seconds}s ${ms.toLocaleString('en-US')}ms`; + } + + return `${ms.toLocaleString('en-US')}ms`; +} diff --git a/packages/diagnostic/src/-define.ts b/packages/diagnostic/src/-define.ts new file mode 100644 index 00000000000..184937b1473 --- /dev/null +++ b/packages/diagnostic/src/-define.ts @@ -0,0 +1,124 @@ +import type { ModuleCallback, ModuleInfo, OrderedMap, TestCallback, TestContext, TestInfo } from './-types'; +import { assert, generateHash } from './-utils'; +import { Config, getCurrentModule, HooksDelegate, setCurrentModule } from './internals/config'; + +export { registerReporter } from './internals/delegating-reporter'; +export { setupGlobalHooks, configure } from './internals/config'; +export { PublicTestInfo } from './internals/run'; + +export const Modules: OrderedMap> = { + byName: new Map(), + byOrder: [], +}; + +export type { Diagnostic, Hooks as NestedHooks, GlobalHooks, TestContext } from './-types'; + +export function module(name: string, cb: ModuleCallback): void { + const parentModule = getCurrentModule() ?? null; + let moduleName = name; + if (parentModule) { + moduleName = `${parentModule.name} > ${name}`; + } else { + Config.totals.primaryModules++; + } + Config.totals.modules++; + + assert(`Cannot add the same module name twice: ${moduleName}`, !Modules.byName.has(moduleName)); + const moduleConfig: ModuleInfo['config'] = { + beforeEach: [], + afterEach: [], + beforeModule: [], + afterModule: [], + }; + const tests: OrderedMap> = { byName: new Map(), byOrder: [] }; + const modules: OrderedMap> = { byName: new Map(), byOrder: [] }; + const moduleInfo = { + id: generateHash(moduleName), + moduleName, + name, + skipped: null, + cb, + config: moduleConfig, + tests, + modules, + parent: parentModule, + }; + + setCurrentModule(moduleInfo); + + if (parentModule) { + parentModule.modules.byName.set(name, moduleInfo); + parentModule.modules.byOrder.push(moduleInfo); + } else { + // @ts-expect-error TS poorly handles subtype constraints + Modules.byName.set(name, moduleInfo); + // @ts-expect-error TS poorly handles subtype constraints + Modules.byOrder.push(moduleInfo); + } + + cb(HooksDelegate); + setCurrentModule(parentModule as unknown as ModuleInfo); +} + +export function test(name: string, cb: TestCallback): void { + const currentModule = getCurrentModule(); + assert(`Cannot add a test outside of a module`, !!currentModule); + assert(`Cannot add the same test name twice: ${name}`, !currentModule.tests.byName.has(name)); + Config.totals.tests++; + + const testName = currentModule.moduleName + ' > ' + name; + const testInfo = { + id: generateHash(testName), + name, + testName, + cb, + skip: false, + todo: false, + module: currentModule, + }; + + currentModule.tests.byName.set(name, testInfo); + currentModule.tests.byOrder.push(testInfo); +} + +export function todo(name: string, cb: TestCallback): void { + const currentModule = getCurrentModule(); + assert(`Cannot add a test outside of a module`, !!currentModule); + assert(`Cannot add the same test name twice: ${name}`, !currentModule.tests.byName.has(name)); + Config.totals.todo++; + + const testName = currentModule.moduleName + ' > ' + name; + const testInfo = { + id: generateHash(testName), + name, + testName, + cb, + skip: false, + todo: true, + module: currentModule, + }; + + currentModule.tests.byName.set(name, testInfo); + currentModule.tests.byOrder.push(testInfo); +} + +export function skip(name: string, cb: TestCallback): void { + const currentModule = getCurrentModule(); + assert(`Cannot add a test outside of a module`, !!currentModule); + assert(`Cannot add the same test name twice: ${name}`, !currentModule.tests.byName.has(name)); + Config.totals.skipped++; + + const testName = currentModule.moduleName + ' > ' + name; + const testInfo = { + id: generateHash(testName), + name, + testName, + cb, + skip: true, + todo: false, + module: currentModule, + }; + + currentModule.tests.byName.set(name, testInfo); + currentModule.tests.byOrder.push(testInfo); +} diff --git a/packages/diagnostic/src/-ember/is-component.ts b/packages/diagnostic/src/-ember/is-component.ts new file mode 100644 index 00000000000..cab683dbeaa --- /dev/null +++ b/packages/diagnostic/src/-ember/is-component.ts @@ -0,0 +1,19 @@ +// @ts-expect-error: types for this API is not consistently available (via transitive +// deps) and we do not currently want to make it an explicit dependency. It +// does, however, consistently work at runtime. :sigh: +import { getInternalComponentManager as getComponentManager } from '@glimmer/manager'; + +export type ComponentLike = object; + +/** + * We should ultimately get a new API from @glimmer/runtime that provides this functionality + * (see https://github.com/emberjs/rfcs/pull/785 for more info). + * @private + * @param {Object} maybeComponent The thing you think might be a component + * @returns {boolean} True if it's a component, false if not + */ +function isComponent(maybeComponent: object): maybeComponent is ComponentLike { + return !!(getComponentManager as (c: object, v: boolean) => object)(maybeComponent, true); +} + +export default isComponent; diff --git a/packages/diagnostic/src/-types.ts b/packages/diagnostic/src/-types.ts new file mode 100644 index 00000000000..f840abbf39b --- /dev/null +++ b/packages/diagnostic/src/-types.ts @@ -0,0 +1,145 @@ +import type { SuiteReport } from './-types/report'; + +export type CompatTestReport = { + id: number; + name: string; + items: { passed: boolean; message: string }[]; + failed: number; + passed: number; + total: number; + runDuration: number; + skipped: boolean; + todo: boolean; + testId: string; +}; + +export interface Emitter { + emit(name: 'suite-start' | 'suite-finish', data: SuiteReport): void; + emit(name: 'test-start' | 'test-finish', data: CompatTestReport): void; +} + +export type ParamConfig = { + id: string; + label: string; + value: boolean | string; +}; + +export type GlobalHooksStorage = { + onSuiteStart: GlobalCallback[]; + onSuiteFinish: GlobalCallback[]; + beforeModule: GlobalCallback[]; + afterModule: GlobalCallback[]; + beforeEach: HooksCallback[]; + afterEach: HooksCallback[]; +}; + +export type GlobalConfig = { + params: { + [key in + | 'search' + | 'concurrency' + | 'tryCatch' + | 'instrument' + | 'hideReport' + | 'memory' + | 'groupLogs' + | 'debug' + | 'container']: ParamConfig; + }; + tests: Set; + modules: Set; + _current: SuiteReport | null; + useTestem: boolean; + useDiagnostic: boolean; + testTimeoutMs: number; + concurrency: number; + globalHooks: GlobalHooksStorage; + totals: { + tests: number; + primaryModules: number; + modules: number; + skipped: number; + todo: number; + }; +}; + +export interface Diagnostic { + equal(actual: T, expected: T, message?: string): void; + notEqual(actual: T, expected: T, message?: string): void; + deepEqual(actual: T, expected: T, message?: string): void; + notDeepEqual(actual: T, expected: T, message?: string): void; + throws(fn: () => Promise, expected?: string | RegExp, message?: string): Promise; + throws(fn: () => void, expected?: string | RegExp, message?: string): void; + doesNotThrow(fn: () => Promise, expected?: string | RegExp, message?: string): Promise; + doesNotThrow(fn: () => void, expected?: string | RegExp, message?: string): void; + true(actual: boolean, message?: string): void; + false(actual: boolean, message?: string): void; + ok(actual: unknown, message?: string): void; + notOk(actual: unknown, message?: string): void; + expect(count: number): void; + step(name: string): void; + verifySteps(steps: string[], message?: string): void; + /** + * Asserts that the actual value has at least the properties of the expected value. + * If additional properties are present on the actual value, they are ignored. + * + * @param actual + * @param expected + * @param message + */ + satisfies(actual: J, expected: T, message?: string): void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TestContext {} + +export type GlobalCallback = () => void | Promise; + +export interface Hooks { + beforeEach: (cb: HooksCallback) => void; + afterEach: (cb: HooksCallback) => void; + beforeModule: (cb: GlobalCallback) => void; + afterModule: (cb: GlobalCallback) => void; +} +export interface GlobalHooks extends Hooks { + onSuiteStart: (cb: GlobalCallback) => void; + onSuiteFinish: (cb: GlobalCallback) => void; +} + +export type HooksCallback = (this: TC, assert: Diagnostic) => void | Promise; +export type ModuleCallback = ((hooks: Hooks) => void) | (() => void); +export type TestCallback = (this: TC, assert: Diagnostic) => void | Promise; + +export interface TestInfo { + /* A unique id for the test based on the hash of the full testName */ + id: string; + /* The name of the test, not including moduleName */ + name: string; + /* The full name of the test including moduleName */ + testName: string; + cb: TestCallback; + skip: boolean; + todo: boolean; + module: ModuleInfo; +} + +export interface OrderedMap { + byName: Map; + byOrder: T[]; +} + +export interface ModuleInfo { + id: string; + skipped: boolean | null; + moduleName: string; + name: string; + cb: ModuleCallback; + config: { + beforeEach: HooksCallback[]; + afterEach: HooksCallback[]; + beforeModule: GlobalCallback[]; + afterModule: GlobalCallback[]; + }; + tests: OrderedMap>; + modules: OrderedMap>; +} diff --git a/packages/diagnostic/src/-types/report.ts b/packages/diagnostic/src/-types/report.ts new file mode 100644 index 00000000000..46e8a79f509 --- /dev/null +++ b/packages/diagnostic/src/-types/report.ts @@ -0,0 +1,57 @@ +export interface SuiteReport { + totals: { + tests: number; + primaryModules: number; + modules: number; + skipped: number; + todo: number; + }; + passed: number; + failed: number; + skipped: number; + todo: number; + start: PerformanceMark | null; + end: PerformanceMark | null; + measure: PerformanceMeasure | PerformanceMark | null; +} +export interface TestReport { + id: string; + name: string; + skipped: boolean; + todo: boolean; + start: PerformanceMark | null; + end: PerformanceMark | null; + measure: PerformanceMeasure | PerformanceMark | null; + result: { + diagnostics: DiagnosticReport[]; + passed: boolean; + failed: boolean; + }; + module: ModuleReport; +} +export interface ModuleReport { + name: string; + start: PerformanceMark | null; + end: PerformanceMark | null; + measure: PerformanceMeasure | PerformanceMark | null; + passed: boolean; + failed: boolean; +} +export interface DiagnosticReport { + testId: string; + message: string; + passed: boolean; + expected: unknown; + actual: unknown; + stack: string; +} + +export interface Reporter { + onSuiteStart: (report: SuiteReport) => void; + onSuiteFinish: (report: SuiteReport) => void; + onTestStart: (test: TestReport) => void; + onTestFinish: (test: TestReport) => void; + onModuleStart: (module: ModuleReport) => void; + onModuleFinish: (module: ModuleReport) => void; + onDiagnostic: (diagnostic: DiagnosticReport) => void; +} diff --git a/packages/diagnostic/src/-utils.ts b/packages/diagnostic/src/-utils.ts new file mode 100644 index 00000000000..31dde59ef91 --- /dev/null +++ b/packages/diagnostic/src/-utils.ts @@ -0,0 +1,71 @@ +/* global window, globalThis, global, self */ +import type { GlobalHooksStorage, HooksCallback, ModuleInfo, TestContext } from './-types'; + +export function assert(message: string, test: unknown): asserts test { + if (!test) { + throw new Error(message); + } +} + +export function getGlobal(): typeof globalThis { + // prettier-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const g: typeof globalThis | null = + typeof globalThis !== 'undefined' ? globalThis + : typeof window !== 'undefined' ? window + // @ts-expect-error global is node only + : typeof global !== 'undefined' ? global + : typeof self !== 'undefined' ? self + : null; + + assert(`Expected to find a global object`, g !== null); + return g as unknown as typeof globalThis; +} + +export function getChain( + globalHooks: GlobalHooksStorage, + module: ModuleInfo, + parents: ModuleInfo[] | null, + prop: 'beforeEach' | 'afterEach' +): HooksCallback[] { + const chain: HooksCallback[] = []; + + if (globalHooks[prop].length) { + chain.push(...globalHooks[prop]); + } + + if (parents) { + for (const parent of parents) { + if (parent.config[prop].length) { + chain.push(...parent.config[prop]); + } + } + } + if (module.config[prop].length) { + chain.push(...module.config[prop]); + } + + if (prop === 'afterEach') { + chain.reverse(); + } + + return chain; +} + +export function generateHash(str: string) { + let hash = 0; + + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; + } + + // Convert the possibly negative integer hash code into an 8 character hex string, which isn't + // strictly necessary but increases user understanding that the id is a SHA-like hash + let hex = (0x100000000 + hash).toString(16); + if (hex.length < 8) { + hex = '0000000' + hex; + } + + return hex.slice(-8); +} diff --git a/packages/diagnostic/src/ember.ts b/packages/diagnostic/src/ember.ts new file mode 100644 index 00000000000..fcde835905e --- /dev/null +++ b/packages/diagnostic/src/ember.ts @@ -0,0 +1,287 @@ +import { run } from '@ember/runloop'; +import type { SetupContextOptions, TestContext as EmberTestContext } from '@ember/test-helpers'; +import { getTestMetadata, hasCalledSetupRenderingContext, setupContext, teardownContext } from '@ember/test-helpers'; +import type { Owner } from '@ember/test-helpers/build-owner'; + +import { hbs } from 'ember-cli-htmlbars'; +import AbstractTestLoader from 'ember-cli-test-loader/test-support/index'; + +import { module as _module, skip as _skip, test as _test, todo as _todo } from './-define'; +import isComponent from './-ember/is-component'; +import type { Hooks, ModuleCallback, TestCallback } from './-types'; +import { setupGlobalHooks } from './internals/config'; +import { PublicTestInfo } from './internals/run'; + +// const OUTLET_TEMPLATE = hbs`{{outlet}}`; +const INVOKE_PROVIDED_COMPONENT = hbs`` as object; + +export interface TestContext extends EmberTestContext { + element: HTMLDivElement; +} +export interface RenderingTestContext extends TestContext { + [hasCalledSetupRenderingContext]: boolean; + render(template: object): Promise; +} + +export function module(name: string, cb: ModuleCallback): void { + return _module(name, cb); +} + +export function test(name: string, cb: TestCallback): void { + return _test(name, cb); +} + +export function skip(name: string, cb: TestCallback): void { + return _skip(name, cb); +} + +export function todo(name: string, cb: TestCallback): void { + return _todo(name, cb); +} + +type RegistryType = 'component' | 'helper' | 'modifier' | 'service' | 'template' | 'route' | 'controller' | 'model'; +type RegistryKey = + | `${RegistryType}:${string}` + | '-environment:main' + | 'event_dispatcher:main' + | 'outlet-view:main' + | '-top-level-view:main' + | 'view:-outlet'; + +type FullOwner = Owner & { + factoryFor: (name: RegistryKey) => { create(args: object): unknown }; + lookup: (name: RegistryKey) => unknown; + register: (name: RegistryKey, value: unknown) => void; +}; + +// fix bug with embroider/webpack/auto-import and test-loader +// prettier-ignore +// @ts-expect-error +const CLITestLoader: typeof AbstractTestLoader = AbstractTestLoader.default + // @ts-expect-error + ? AbstractTestLoader.default as typeof AbstractTestLoader + : AbstractTestLoader; + +export function setupTest(hooks: Hooks, opts?: SetupContextOptions) { + const options = { waitForSettled: false, ...opts }; + + hooks.beforeEach(async function () { + const testMetadata = getTestMetadata(this); + testMetadata.framework = 'qunit'; + + await setupContext(this, Object.assign({}, options)); + }); + + hooks.afterEach(function (this: TestContext) { + return teardownContext(this, options); + }); +} + +type Outlet = { appendTo: (element: Element) => void; setOutletState: (state: object) => void }; + +function upgradeContext(context: TestContext): asserts context is RenderingTestContext & { + [PublicTestInfo]: { id: string; name: string }; + rootElement: HTMLDivElement; +} { + (context as unknown as RenderingTestContext)[hasCalledSetupRenderingContext] = true; +} + +function upgradeOwner(owner: Owner): asserts owner is FullOwner {} + +export function setupRenderingTest(hooks: Hooks, options: SetupContextOptions = {}) { + const _options = { waitForSettled: false, ...options } as unknown as SetupContextOptions & { + rootElement: HTMLDivElement; + waitForSettled: boolean; + }; + + hooks.beforeEach(async function () { + upgradeContext(this); + this.render = (template: object) => render(this, template); + const opts = Object.assign({}, _options); + const testMetadata = getTestMetadata(this); + testMetadata.setupTypes.push('setupRenderingContext'); + testMetadata.framework = 'qunit'; + + const container = document.getElementById('ember-testing'); + const testContainer = document.createElement('div'); + testContainer.className = 'ember-test-container'; + container!.appendChild(testContainer); + opts.rootElement = testContainer; + this.rootElement = testContainer; + + await setupContext(this, opts); + + const { owner } = this; + upgradeOwner(owner); + + const OutletView = owner.factoryFor('view:-outlet')!; + const environment = owner.lookup('-environment:main'); + const template = owner.lookup('template:-outlet'); + testContainer.setAttribute('test-id', this[PublicTestInfo].id); + testContainer.setAttribute('test-name', this[PublicTestInfo].name); + + const toplevelView = OutletView.create({ + template, + environment, + }) as Outlet; + + owner.register('-top-level-view:main', { + create() { + return toplevelView; + }, + }); + toplevelView.appendTo(testContainer); + + Object.defineProperty(this, 'element', { + configurable: true, + enumerable: true, + value: testContainer, + writable: false, + }); + }); + + hooks.afterEach(async function (this: TestContext) { + await teardownContext(this, _options); + upgradeContext(this); + this.rootElement.remove(); + }); +} + +let moduleLoadFailures: Error[] = []; + +class TestLoader extends CLITestLoader { + moduleLoadFailure(moduleName: string, error: Error) { + moduleLoadFailures.push(error); + } +} + +/** + Load tests following the default patterns: + + * The module name ends with `-test` + * The module name ends with `.jshint` + + @method loadTests + */ +function loadTests() { + TestLoader.load(); +} + +export function configure() { + setupGlobalHooks((hooks) => { + hooks.onSuiteFinish(() => { + const length = moduleLoadFailures.length; + + try { + if (length === 0) { + // do nothing + } else if (length === 1) { + throw moduleLoadFailures[0]; + } else { + throw new Error('\n' + moduleLoadFailures.join('\n')); + } + } finally { + // ensure we release previously captured errors. + moduleLoadFailures = []; + } + }); + }); + + loadTests(); +} + +export function isRenderingTestContext(context: TestContext): context is RenderingTestContext { + return hasCalledSetupRenderingContext in context; +} + +function isTemplateFunction(template: unknown): template is (owner: Owner) => object { + return typeof template === 'function'; +} + +function lookupTemplate(owner: Owner, templateFullName: RegistryKey): object | undefined { + upgradeOwner(owner); + const template = owner.lookup(templateFullName) as object | ((owner: Owner) => object) | undefined; + if (isTemplateFunction(template)) return template(owner); + return template; +} + +function lookupOutletTemplate(owner: Owner): object { + upgradeOwner(owner); + const OutletTemplate = lookupTemplate(owner, 'template:-outlet'); + if (!OutletTemplate) { + throw new Error(`Could not find -outlet template`); + // owner.register('template:-outlet', OUTLET_TEMPLATE); + // OutletTemplate = lookupTemplate(owner, 'template:-outlet'); + } + + return OutletTemplate; +} + +let templateId = 0; +// eslint-disable-next-line @typescript-eslint/require-await +export async function render(context: TestContext, template: object): Promise { + if (!template) { + throw new Error('you must pass a template to `render()`'); + } + + if (!context || !isRenderingTestContext(context)) { + throw new Error('Cannot call `render` without having first called `setupRenderingContext`.'); + } + + const { owner } = context; + upgradeOwner(owner); + const testMetadata = getTestMetadata(context); + testMetadata.usedHelpers.push('render'); + + // SAFETY: this is all wildly unsafe, because it is all using private API. + // At some point we should define a path forward for this kind of internal + // API. For now, just flagging it as *NOT* being safe! + + const toplevelView = owner.lookup('-top-level-view:main') as Outlet; + const OutletTemplate = lookupOutletTemplate(owner); + + let controllerContext: object = context; + if (isComponent(template)) { + controllerContext = { + ProvidedComponent: template, + }; + template = INVOKE_PROVIDED_COMPONENT; + } + + templateId += 1; + const templateFullName = `template:-undertest-${templateId}` as const; + owner.register(templateFullName, template); + const finalTemplate = lookupTemplate(owner, templateFullName); + + const outletState = { + render: { + owner, + into: undefined, + outlet: 'main', + name: 'application', + controller: undefined, + ViewClass: undefined, + template: OutletTemplate, + }, + + outlets: { + main: { + render: { + owner, + into: undefined, + outlet: 'main', + name: 'index', + controller: controllerContext, + ViewClass: undefined, + template: finalTemplate, + outlets: {}, + }, + outlets: {}, + }, + }, + }; + + run(() => { + toplevelView.setOutletState(outletState); + }); +} diff --git a/packages/diagnostic/src/emitters/diagnostic.ts b/packages/diagnostic/src/emitters/diagnostic.ts new file mode 100644 index 00000000000..21f724a0df2 --- /dev/null +++ b/packages/diagnostic/src/emitters/diagnostic.ts @@ -0,0 +1,117 @@ +import type { CompatTestReport, Emitter } from '../-types'; +import type { SuiteReport } from '../-types/report'; +import { assert } from '../-utils'; + +type EmitEvent = { + name: 'suite-start' | 'suite-finish' | 'test-start' | 'test-finish'; + data: SuiteReport | CompatTestReport; +}; + +class DiagnosticEmitter implements Emitter { + socket: WebSocket; + connected: boolean; + buffer: EmitEvent[] = []; + browserId: string; + windowId: string; + + constructor() { + // A test url might look like + // http://localhost:7537/1984/1/tests/index.html?hidepassed&filter=foo + // where 1984 is the browserId and 1 is the windowId + const params = new URLSearchParams(window.location.search); + const host = window.location.host; + const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + const browserId = params.get('b')!; + const windowId = params.get('w')!; + const url = `${protocol}${host}/ws`; + this.browserId = browserId; + this.windowId = windowId; + this.connected = false; + this.buffer = []; + + if (!browserId || !windowId) { + console.warn( + `[Diagnostic] Expected to find a browserId and windowId in the url. Likely this page was not served by the diagnostic server. Remote reporting will not be available.` + ); + this.socket = null as unknown as WebSocket; + return; + } + + console.log(`[Diagnostic] Attempting to connect to remote reporter at ${url}`); + try { + const socket = new WebSocket(url); + socket.onopen = (_event) => { + console.log(`[Diagnostic] Remote Reporter Connection established`); + this.connected = true; + this.buffer.forEach((event) => { + this.emit(event.name as 'suite-start', event.data as SuiteReport); + }); + this.buffer = []; + }; + + socket.onclose = (event) => { + this.connected = false; + if (event.wasClean) { + console.log( + `[Diagnostic] Remote Reporter Connection closed cleanly, code=${event.code} reason=${event.reason}` + ); + } else { + console.error(`[Diagnostic] Remote Reporter Connection Died`, event); + } + }; + socket.onerror = (e) => { + this.connected = false; + console.error(e); + throw new Error(`[Diagnostic] Remote Reporter Connection Failed`); + }; + socket.onmessage = (message: MessageEvent) => { + const msg = JSON.parse(message.data) as { name: 'close' | 'reload' }; + if (msg.name === 'close') { + window.close(); + } else if (msg.name === 'reload') { + window.location.reload(); + } else { + throw new Error(`[Diagnostic] Unexpected message from server`); + } + }; + + this.socket = socket; + } catch (e) { + console.error(`[Diagnostic] Unexpected error trying to connect`, e); + this.socket = null as unknown as WebSocket; + } + } + + emit(name: 'suite-start' | 'suite-finish', data: SuiteReport): void; + emit(name: 'test-start' | 'test-finish', data: CompatTestReport): void; + emit(name: 'suite-start' | 'suite-finish' | 'test-start' | 'test-finish', data: SuiteReport | CompatTestReport) { + if (!this.socket) { + return; + } + if (!this.connected) { + this.buffer.push({ name, data }); + return; + } + + assert( + `Expected event.name to be one of 'suite-start', 'suite-finish', 'test-start' or 'test-finish'`, + ['suite-start', 'suite-finish', 'test-start', 'test-finish'].includes(name) + ); + assert(`Expected event.data to be defined`, typeof data !== 'undefined'); + const event = { browserId: this.browserId, windowId: this.windowId, name, data, timestamp: Date.now() }; + + this.socket.send(JSON.stringify(event)); + } +} + +// function getRelativeTimeStamp(timestamp: number) { +// const now = Date.now(); +// const perfNow = performance.now(); + +// const diff = perfNow - timestamp; +// return now - diff; +// } + +export function createDiagnosticEmitter(): Promise { + return Promise.resolve(new DiagnosticEmitter()); +} diff --git a/packages/diagnostic/src/emitters/testem.ts b/packages/diagnostic/src/emitters/testem.ts new file mode 100644 index 00000000000..2f5d8b7c3b2 --- /dev/null +++ b/packages/diagnostic/src/emitters/testem.ts @@ -0,0 +1,57 @@ +import type { CompatTestReport, Emitter } from '../-types'; +import type { SuiteReport } from '../-types/report'; +import { assert } from '../-utils'; + +type TestemSocket = { + emit(name: 'tests-start' | 'all-test-results'): void; // suite-start / suite-finish + emit(name: 'tests-start' | 'test-result', data: CompatTestReport): void; // test-start / test-finish +}; + +interface TestemGlobal { + useCustomAdapter(callback: (socket: TestemSocket) => void): void; +} + +class TestemEmitter implements Emitter { + socket: TestemSocket; + + constructor(socket: TestemSocket) { + this.socket = socket; + } + + emit(name: 'suite-start' | 'suite-finish', data: SuiteReport): void; + emit(name: 'test-start' | 'test-finish', data: CompatTestReport): void; + emit(name: 'suite-start' | 'suite-finish' | 'test-start' | 'test-finish', data: SuiteReport | CompatTestReport) { + assert( + `Expected event.name to be one of 'suite-start', 'suite-finish', 'test-start' or 'test-finish'`, + ['suite-start', 'suite-finish', 'test-start', 'test-finish'].includes(name) + ); + assert(`Expected event.data to be defined`, typeof data !== 'undefined'); + + if (name === 'suite-start') { + this.socket.emit('tests-start'); + } else if (name === 'suite-finish') { + this.socket.emit('all-test-results'); + } else if (name === 'test-start') { + this.socket.emit('tests-start', data as CompatTestReport); + } else if (name === 'test-finish') { + this.socket.emit('test-result', data as CompatTestReport); + } + } +} + +export function createTestemEmitter(): Promise { + return new Promise((resolve, reject) => { + // @ts-expect-error + const _Testem: TestemGlobal = window.Testem as TestemGlobal; + const hasTestem = typeof _Testem !== 'undefined'; + + if (!hasTestem) { + return reject(new Error('Testem is not present on the page.')); + } + + _Testem.useCustomAdapter((socket: TestemSocket) => { + const emitter = new TestemEmitter(socket); + resolve(emitter); + }); + }); +} diff --git a/packages/diagnostic/src/index.ts b/packages/diagnostic/src/index.ts new file mode 100644 index 00000000000..87a49d153b8 --- /dev/null +++ b/packages/diagnostic/src/index.ts @@ -0,0 +1,99 @@ +import type { TestContext } from './-define'; +import { Modules } from './-define'; +import type { ModuleInfo } from './-types'; +import type { SuiteReport } from './-types/report'; +import { Config, instrument } from './internals/config'; +import { DelegatingReporter } from './internals/delegating-reporter'; +import { runModule } from './internals/run'; + +export { registerReporter } from './internals/delegating-reporter'; +export { setupGlobalHooks, configure } from './internals/config'; +export { PublicTestInfo } from './internals/run'; + +export { module, test, todo, skip } from './-define'; + +function shouldSkipModule(module: ModuleInfo): boolean { + // if we have no filters, we should run everything + if (!Config.modules.size && !Config.tests.size) { + module.skipped = false; + return false; + } + + // if we have specific tests, only run if the test is in the list + // or a descendent list + if (Config.tests.size) { + let found = false; + for (const test of module.tests.byOrder) { + if (Config.tests.has(test.id)) { + found = true; + break; + } + } + if (!found) { + for (const subModule of module.modules.byOrder) { + if (!shouldSkipModule(subModule)) { + found = true; + break; + } + } + } + module.skipped = !found; + return !found; + } + + // if we have specific modules, only run if the module is in the list + // or a descendent list + if (Config.modules.has(module.id)) { + module.skipped = false; + return false; + } + + let found = false; + for (const subModule of module.modules.byOrder) { + if (!shouldSkipModule(subModule)) { + found = true; + break; + } + } + module.skipped = !found; + return !found; +} + +export async function start() { + const report: SuiteReport = { + totals: Object.assign({}, Config.totals), + passed: 0, + failed: 0, + skipped: 0, + todo: 0, + start: null, + end: null, + measure: null, + }; + Config._current = report; + report.start = instrument() && performance.mark('@warp-drive/diagnostic:start'); + + DelegatingReporter.onSuiteStart(report); + for (const hook of Config.globalHooks.onSuiteStart) { + await hook(); + } + + const promises: Promise[] = []; + for (const _module of Modules.byOrder) { + if (shouldSkipModule(_module)) { + continue; + } + await runModule(_module, null, promises); + } + if (promises.length) { + await Promise.all(promises); + } + + for (const hook of Config.globalHooks.onSuiteFinish) { + await hook(); + } + report.end = instrument() && performance.mark('@warp-drive/diagnostic:end'); + report.measure = + instrument() && performance.measure('@warp-drive/diagnostic:run', report.start.name, report.end.name); + DelegatingReporter.onSuiteFinish(report); +} diff --git a/packages/diagnostic/src/internals/config.ts b/packages/diagnostic/src/internals/config.ts new file mode 100644 index 00000000000..0a2c4a1b7f7 --- /dev/null +++ b/packages/diagnostic/src/internals/config.ts @@ -0,0 +1,236 @@ +/* global Testem */ +import type { + GlobalCallback, + GlobalConfig, + GlobalHooks, + HooksCallback, + ModuleInfo, + ParamConfig, + TestContext, +} from '../-types'; +import { assert } from '../-utils'; + +const urlParams = new URLSearchParams(window.location.search); + +const search = urlParams.get('search'); +const tests = new Set(urlParams.getAll('t')); +const modules = new Set(urlParams.getAll('m')); + +export const Config: GlobalConfig = { + globalHooks: { + beforeEach: [], + afterEach: [], + beforeModule: [], + afterModule: [], + onSuiteStart: [], + onSuiteFinish: [], + }, + // @ts-expect-error + useTestem: typeof Testem !== 'undefined', + // @ts-expect-error + useDiagnostic: typeof Testem === 'undefined', + testTimeoutMs: 50, + concurrency: 1, + modules, + tests, + params: { + search: { + id: 'search', + label: 'Filter Tests', + value: search ?? '', + }, + hideReport: { + id: 'hideReport', + label: 'Hide Report', + value: true, + }, + concurrency: { + id: 'concurrency', + label: 'Enable Concurrency', + value: false, + }, + memory: { + id: 'memory', + label: 'Instrument Memory', + value: false, + }, + instrument: { + id: 'performance', + label: 'Instrument Performance', + value: true, + }, + groupLogs: { + id: 'groupLogs', + label: 'Group Logs', + value: true, + }, + debug: { + id: 'debug', + label: 'Debug Mode', + value: false, + }, + container: { + id: 'container', + label: 'Hide Container', + value: true, + }, + tryCatch: { + id: 'tryCatch', + label: 'No Try/Catch', + value: true, + }, + }, + totals: { + tests: 0, + primaryModules: 0, + modules: 0, + skipped: 0, + todo: 0, + }, + _current: null, +}; + +let currentModule: ModuleInfo; +let isResolvingGlobalHooks = false; +export const HooksDelegate = { + beforeEach(cb: HooksCallback): void { + if (isResolvingGlobalHooks) { + // @ts-expect-error TS poorly handles subtype constraints + Config.globalHooks.beforeEach.push(cb); + } else { + // @ts-expect-error TS poorly handles subtype constraints + currentModule.config.beforeEach.push(cb); + } + }, + afterEach(cb: HooksCallback): void { + if (isResolvingGlobalHooks) { + // @ts-expect-error TS poorly handles subtype constraints + Config.globalHooks.afterEach.push(cb); + } else { + // @ts-expect-error TS poorly handles subtype constraints + currentModule.config.afterEach.push(cb); + } + }, + beforeModule(cb: GlobalCallback): void { + if (isResolvingGlobalHooks) { + Config.globalHooks.beforeModule.push(cb); + } else { + currentModule.config.beforeModule.push(cb); + } + }, + afterModule(cb: GlobalCallback): void { + if (isResolvingGlobalHooks) { + Config.globalHooks.afterModule.push(cb); + } else { + currentModule.config.afterModule.push(cb); + } + }, + onSuiteStart(cb: GlobalCallback): void { + assert(`Cannot add a global onSuiteStart hook inside of a module`, isResolvingGlobalHooks); + Config.globalHooks.onSuiteStart.push(cb); + }, + onSuiteFinish(cb: GlobalCallback): void { + assert(`Cannot add a global onSuiteFinish hook inside of a module`, isResolvingGlobalHooks); + Config.globalHooks.onSuiteFinish.push(cb); + }, +}; + +export function getCurrentModule(): ModuleInfo { + return currentModule; +} + +export function setCurrentModule(module: ModuleInfo) { + // @ts-expect-error TS poorly handles subtype constraints + currentModule = module; +} + +export function setupGlobalHooks(cb: (hooks: GlobalHooks) => void): void { + isResolvingGlobalHooks = true; + cb(HooksDelegate); + isResolvingGlobalHooks = false; +} + +export type ConfigOptions = { + concurrency: number; + instrument: boolean; + tryCatch: boolean; + debug: boolean; + groupLogs: boolean; + memory: boolean; + container: boolean; + hideReport: boolean; + params: Record; + useTestem: boolean; + useDiagnostic: boolean; + testTimeoutMs: number; +}; +const configOptions = [ + 'concurrency', + 'tryCatch', + 'instrument', + 'hideReport', + 'memory', + 'groupLogs', + 'debug', + 'container', +] as const; +export function configure(options: Partial): void { + if (options.useTestem && options.useDiagnostic) { + throw new Error( + `Cannot use both Testem and Diagnostic at the same time. Please remove one of these options or set it to false.` + ); + } + if ('useTestem' in options && typeof options.useTestem === 'boolean') { + Config.useTestem = options.useTestem; + Config.useDiagnostic = !options.useTestem; + } + if ('useDiagnostic' in options && typeof options.useDiagnostic === 'boolean') { + Config.useDiagnostic = options.useDiagnostic; + Config.useTestem = !options.useDiagnostic; + } + + if ('concurrency' in options && typeof options.concurrency === 'number') { + Config.concurrency = options.concurrency; + // @ts-expect-error + options.concurrency = options.concurrency > 1; + } + configOptions.forEach((key) => { + if (key in options && typeof options[key] === 'boolean') { + Config.params[key].value = options[key]; + } + // don't allow setting these params via configure + if (options.params?.[key]) { + delete options.params[key]; + } + }); + + Config.testTimeoutMs = options.testTimeoutMs ?? 0; + + // copy over any remaining params + Object.assign(Config.params, options.params); +} + +export function getSettings() { + return { + useTestem: Config.useTestem, + useDiagnostic: Config.useDiagnostic, + concurrency: Config.concurrency, + params: Config.params, + }; +} + +export function instrument() { + return (Config.params.instrument.value || null) as unknown as PerformanceMark; +} + +export function groupLogs() { + return Config.params.groupLogs.value; +} + +// 0 - stop +// 1 - start +// 2 - back +// 3 - forward +// 4 - restart +export function updateSuiteState(value: number) {} +export function updateConfigValue(key: string, value: boolean) {} diff --git a/packages/diagnostic/src/internals/delegating-reporter.ts b/packages/diagnostic/src/internals/delegating-reporter.ts new file mode 100644 index 00000000000..8b0163c65a6 --- /dev/null +++ b/packages/diagnostic/src/internals/delegating-reporter.ts @@ -0,0 +1,59 @@ +import type { Reporter, SuiteReport } from '../-types/report'; + +const Reporters = new Set(); +export function registerReporter(reporter: Reporter) { + Reporters.add(reporter); +} + +let activeSuite: SuiteReport; +export const DelegatingReporter: Reporter = { + onSuiteStart(report) { + if (activeSuite) { + throw new Error(`Cannot start a test suite while another suite is active`); + } + activeSuite = report; + for (const reporter of Reporters) { + reporter.onSuiteStart(report); + } + }, + onSuiteFinish(report) { + activeSuite = null as unknown as SuiteReport; + for (const reporter of Reporters) { + reporter.onSuiteFinish(report); + } + }, + onTestStart(report) { + for (const reporter of Reporters) { + reporter.onTestStart(report); + } + }, + onTestFinish(report) { + activeSuite.passed += report.result.passed ? 1 : 0; + activeSuite.failed += report.result.failed ? 1 : 0; + activeSuite.skipped += report.skipped ? 1 : 0; + activeSuite.todo += report.result.passed && report.todo ? 1 : 0; + + const module = report.module; + module.failed = report.result.failed || module.failed; + module.passed = !module.failed; + + for (const reporter of Reporters) { + reporter.onTestFinish(report); + } + }, + onModuleStart(report) { + for (const reporter of Reporters) { + reporter.onModuleStart(report); + } + }, + onModuleFinish(report) { + for (const reporter of Reporters) { + reporter.onModuleFinish(report); + } + }, + onDiagnostic(report) { + for (const reporter of Reporters) { + reporter.onDiagnostic(report); + } + }, +}; diff --git a/packages/diagnostic/src/internals/diagnostic.ts b/packages/diagnostic/src/internals/diagnostic.ts new file mode 100644 index 00000000000..21208cdd131 --- /dev/null +++ b/packages/diagnostic/src/internals/diagnostic.ts @@ -0,0 +1,383 @@ +import type { GlobalConfig, TestContext, TestInfo } from '../-types'; +import type { DiagnosticReport, Reporter, TestReport } from '../-types/report'; +import equiv from '../legacy/equiv'; + +class InternalCompat { + declare _diagnostic: Diagnostic; + + constructor(diagnostic: Diagnostic) { + this._diagnostic = diagnostic; + } + + get testId() { + return this._diagnostic.__currentTest.id; + } + + get expected() { + return this._diagnostic.expected; + } + set expected(value) { + this._diagnostic.expected = value; + } +} + +export class Diagnostic { + declare __currentTest: TestInfo; + declare __report: TestReport; + declare __config: GlobalConfig; + declare __reporter: Reporter; + declare expected: number | null; + declare _steps: string[]; + + // QUnit private API compat + declare test: InternalCompat; + + constructor(reporter: Reporter, config: GlobalConfig, test: TestInfo, report: TestReport) { + this.__currentTest = test; + this.__report = report; + this.__config = config; + this.__reporter = reporter; + this.expected = null; + this._steps = []; + this.test = new InternalCompat(this); + } + + pushResult( + result: Pick & { result?: boolean } + ): void { + const diagnostic = Object.assign({ passed: result.passed ?? result.result }, result, { + testId: this.__currentTest.id, + }); + this.__report.result.diagnostics.push(diagnostic); + + if (!diagnostic.passed) { + this.__report.result.passed = false; + this.__report.result.failed = true; + } + + this.__reporter.onDiagnostic(diagnostic); + } + + equal(actual: T, expected: T, message?: string): void { + if (actual !== expected) { + if (this.__config.params.tryCatch.value) { + try { + throw new Error(message || `Expected ${String(actual)} to equal ${String(expected)}`); + } catch (err) { + this.pushResult({ + message: message || 'equal', + stack: (err as Error).stack!, + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'equal', + stack: '', + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'equal', + stack: '', + passed: true, + actual: true, + expected: true, + }); + } + } + + notEqual(actual: T, expected: T, message?: string): void { + if (actual === expected) { + if (this.__config.params.tryCatch.value) { + try { + throw new Error(message || `Expected ${String(actual)} to not equal ${String(expected)}`); + } catch (err) { + this.pushResult({ + message: message || 'notEqual', + stack: (err as Error).stack!, + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'notEqual', + stack: '', + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'notEqual', + stack: '', + passed: true, + actual: true, + expected: true, + }); + } + } + + deepEqual(actual: T, expected: T, message?: string): void { + const isEqual = equiv(actual, expected, true); + if (!isEqual) { + if (this.__config.params.tryCatch.value) { + try { + throw new Error(message || `Expected items to be equivalent`); + } catch (err) { + this.pushResult({ + message: message || 'deepEqual', + stack: (err as Error).stack!, + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'deepEqual', + stack: '', + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'deepEqual', + stack: '', + passed: true, + actual: true, + expected: true, + }); + } + } + + /** + * Checks if the actual object satisfies the expected object. + * + * This is a deep comparison that will check if all the properties + * of the expected object are present in the actual object with the + * same values. + * + * This differs from deepEqual in that extra properties on the actual + * object are allowed. + * + * This is great for contract testing APIs that may accept a broader + * object from which a subset of properties are used, or for testing + * higher priority or more stable properties of an object in a dynamic + * environment. + * + * @typedoc + */ + satisfies(actual: J, expected: T, message?: string): void { + const isEqual = equiv(actual, expected, false); + if (!isEqual) { + if (this.__config.params.tryCatch.value) { + try { + throw new Error(message || `Expected items to be equivalent`); + } catch (err) { + this.pushResult({ + message: message || 'satisfies', + stack: (err as Error).stack!, + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'satisfies', + stack: '', + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'satisfies', + stack: '', + passed: true, + actual: true, + expected: true, + }); + } + } + + notDeepEqual(actual: T, expected: T, message?: string): void { + const isEqual = equiv(actual, expected, true); + if (isEqual) { + if (this.__config.params.tryCatch.value) { + try { + throw new Error(message || `Expected items to not be equivalent`); + } catch (err) { + this.pushResult({ + message: message || 'notDeepEqual', + stack: (err as Error).stack!, + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'notDeepEqual', + stack: '', + passed: false, + actual, + expected, + }); + } + } else { + this.pushResult({ + message: message || 'notDeepEqual', + stack: '', + passed: true, + actual: true, + expected: true, + }); + } + } + + true(actual: boolean, message?: string): void { + this.equal(actual, true, message); + } + + false(actual: boolean, message?: string): void { + this.equal(actual, false, message); + } + + ok(actual: unknown, message?: string): void { + this.equal(!!actual, true, message); + } + + notOk(actual: unknown, message?: string): void { + this.equal(!!actual, false, message); + } + + expect(count: number): void { + this.expected = count; + } + + step(name: string): void { + this._steps.push(name); + } + + verifySteps(steps: string[], message?: string): void { + this.deepEqual(this._steps, steps, message); + this._steps = []; + } + + _finalize(): void { + if (this.expected !== null && this.expected !== this.__report.result.diagnostics.length) { + this.pushResult({ + message: `Expected ${this.expected} assertions, but ${this.__report.result.diagnostics.length} were run`, + stack: '', + passed: false, + actual: false, + expected: true, + }); + } + if (this.__report.result.diagnostics.length === 0) { + this.pushResult({ + message: `Expected at least one assertion, but none were run`, + stack: '', + passed: false, + actual: false, + expected: true, + }); + } + if (this._steps.length) { + this.pushResult({ + message: `Expected 0 steps remaining to verify, but ${this._steps.length} were run`, + stack: '', + passed: false, + actual: false, + expected: true, + }); + } + } + + throws(fn: () => Promise, expected?: string | RegExp, message?: string): Promise; + throws(fn: () => void, expected?: string | RegExp, message?: string): void; + throws(fn: () => void | Promise, expected?: string | RegExp, message?: string): Promise | void { + try { + const result = fn(); + const resolved = Promise.resolve(result); + const isPromise = resolved === result; + + if (!isPromise) { + throw new Error(`Expected function to throw ${expected}`); + } + + return resolved.then( + () => { + throw new Error(`Expected function to throw ${expected}`); + }, + (err: Error | string) => { + if (expected) { + if (typeof expected === 'string') { + this.equal(typeof err === 'string' ? err : err.message, expected, message); + } else { + this.equal(typeof err === 'string' ? err : expected.test(err.message), true, message); + } + } + } + ); + } catch (err) { + if (expected) { + if (typeof expected === 'string') { + this.equal(err instanceof Error ? err.message : err, expected, message); + } else { + this.equal(expected.test(err instanceof Error ? err.message : (err as string)), true, message); + } + } + } + } + + doesNotThrow(fn: () => Promise, expected?: string | RegExp, message?: string): Promise; + doesNotThrow(fn: () => void, expected?: string | RegExp, message?: string): void; + doesNotThrow(fn: () => void | Promise, expected?: string | RegExp, message?: string): Promise | void { + try { + const result = fn(); + const resolved = Promise.resolve(result); + const isPromise = resolved === result; + + if (!isPromise) { + return; + } + + return resolved.then( + () => { + return; + }, + (err: Error | string) => { + if (expected) { + if (typeof expected === 'string') { + this.equal(typeof err === 'string' ? err : err.message, expected, message); + } else { + this.equal(expected.test(typeof err === 'string' ? err : err.message), true, message); + } + } + } + ); + } catch (err) { + if (expected) { + if (typeof expected === 'string') { + this.equal(err instanceof Error ? err.message : err, expected, message); + } else { + this.equal(expected.test(err instanceof Error ? err.message : (err as string)), true, message); + } + } + } + } +} diff --git a/packages/diagnostic/src/internals/run.ts b/packages/diagnostic/src/internals/run.ts new file mode 100644 index 00000000000..8f04f0e2a5a --- /dev/null +++ b/packages/diagnostic/src/internals/run.ts @@ -0,0 +1,191 @@ +import type { HooksCallback, ModuleInfo, TestContext, TestInfo } from '../-types'; +import type { ModuleReport, TestReport } from '../-types/report'; +import { getChain } from '../-utils'; +import { Config, groupLogs, instrument } from './config'; +import { DelegatingReporter } from './delegating-reporter'; +import { Diagnostic } from './diagnostic'; + +export const PublicTestInfo = Symbol('TestInfo'); + +function cancellable(promise: Promise, timeout: number): Promise { + return new Promise((resolve, reject) => { + const id = setTimeout(() => { + reject(new Error('Test Timeout Exceeded: ' + timeout + 'ms')); + }, timeout); + promise.then(resolve, reject).finally(() => clearTimeout(id)); + }); +} + +export async function runTest( + moduleReport: ModuleReport, + beforeChain: HooksCallback[], + test: TestInfo, + afterChain: HooksCallback[] +) { + if (Config.tests.size && !Config.tests.has(test.id)) { + return; + } + + const testContext = { + [PublicTestInfo]: { + id: test.id, + name: test.testName, + }, + } as unknown as TC; + const testReport: TestReport = { + id: test.id, + name: test.name, + skipped: test.skip, + todo: test.todo, + start: null, + end: null, + measure: null, + result: { + diagnostics: [], + passed: true, + failed: false, + }, + module: moduleReport, + }; + testReport.start = instrument() && performance.mark(`test:${test.module.moduleName} > ${test.name}:start`); + const Assert = new Diagnostic(DelegatingReporter, Config, test, testReport); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + groupLogs() && console.groupCollapsed(test.name); + DelegatingReporter.onTestStart(testReport); + + if (test.skip) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + groupLogs() && console.groupEnd(); + testReport.end = instrument() && performance.mark(`test:${test.module.moduleName} > ${test.name}:end`); + testReport.measure = + instrument() && + performance.measure(`test:${test.module.moduleName} > ${test.name}`, testReport.start.name, testReport.end.name); + + DelegatingReporter.onTestFinish(testReport); + return; + } + + for (const hook of beforeChain) { + await hook.call(testContext, Assert); + } + + try { + const promise = test.cb.call(testContext, Assert); + + if (promise instanceof Promise && Config.testTimeoutMs > 0) { + await cancellable(promise, Config.testTimeoutMs); + } + + await promise; + } catch (err) { + Assert.pushResult({ + message: `Unexpected Test Failure: ${(err as Error).message}`, + stack: (err as Error).stack!, + passed: false, + actual: false, + expected: true, + }); + if (!Config.params.tryCatch.value) { + throw err; + } + } finally { + for (const hook of afterChain) { + await hook.call(testContext, Assert); + } + Assert._finalize(); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + groupLogs() && console.groupEnd(); + testReport.end = instrument() && performance.mark(`test:${test.module.moduleName} > ${test.name}:end`); + testReport.measure = + instrument() && + performance.measure(`test:${test.module.moduleName} > ${test.name}`, testReport.start.name, testReport.end.name); + + DelegatingReporter.onTestFinish(testReport); + } +} + +export async function runModule( + module: ModuleInfo, + parents: ModuleInfo[] | null, + promises: Promise[] +) { + if (module.skipped) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + groupLogs() && console.groupCollapsed(module.name); + const moduleReport: ModuleReport = { + name: module.moduleName, + start: null, + end: null, + measure: null, + passed: true, + failed: false, + }; + moduleReport.start = instrument() && performance.mark(`module:${module.moduleName}:start`); + + DelegatingReporter.onModuleStart(moduleReport); + for (const hook of Config.globalHooks.beforeModule) { + await hook(); + } + + for (const hook of module.config.beforeModule) { + await hook(); + } + + // run tests + const beforeChain = getChain(Config.globalHooks, module, parents, 'beforeEach'); + const afterChain = getChain(Config.globalHooks, module, parents, 'afterEach'); + + if (Config.params.concurrency.value && Config.concurrency > 1) { + const tests = module.tests.byOrder; + let remainingTests = tests.length; + let currentTest = 0; + + // once remaining tests is 0, we move on + // to the next module after at least one race has completed + while (remainingTests > 0) { + const needed = Config.concurrency - promises.length; + const available = Math.min(needed, remainingTests, Config.concurrency); + for (let i = 0; i < available; i++) { + const test = tests[currentTest++]; + remainingTests--; + const promise = runTest(moduleReport, beforeChain, test, afterChain).finally(() => { + const index = promises.indexOf(promise); + void promises.splice(index, 1); + }); + promises.push(promise); + } + + if (promises.length === Config.concurrency) { + await Promise.race(promises); + } + } + } else { + for (const test of module.tests.byOrder) { + await runTest(moduleReport, beforeChain, test, afterChain); + } + } + + // run modules + for (const childModule of module.modules.byOrder) { + await runModule(childModule, [...(parents || []), module], promises); + } + + for (const hook of module.config.afterModule) { + await hook(); + } + + for (const hook of Config.globalHooks.afterModule) { + await hook(); + } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + groupLogs() && console.groupEnd(); + moduleReport.end = instrument() && performance.mark(`module:${module.moduleName}:end`); + moduleReport.measure = + instrument() && performance.measure(`module:${module.moduleName}`, moduleReport.start.name, moduleReport.end.name); + DelegatingReporter.onModuleFinish(moduleReport); +} diff --git a/packages/diagnostic/src/legacy/equiv.ts b/packages/diagnostic/src/legacy/equiv.ts new file mode 100644 index 00000000000..b71d739418f --- /dev/null +++ b/packages/diagnostic/src/legacy/equiv.ts @@ -0,0 +1,358 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access */ +/* + * The utils below are from QUnit to support deepEqual. + */ +export function objectType(obj: unknown) { + if (typeof obj === 'undefined') { + return 'undefined'; + } + + // Consider: typeof null === object + if (obj === null) { + return 'null'; + } + + const match = toString.call(obj).match(/^\[object\s(.*)\]$/); + const type = match && match[1]; + + switch (type) { + case 'Number': + if (isNaN(obj as number)) { + return 'nan'; + } + return 'number'; + case 'String': + case 'Boolean': + case 'Array': + case 'Set': + case 'Map': + case 'Date': + case 'RegExp': + case 'Function': + case 'Symbol': + return type.toLowerCase(); + default: + return typeof obj; + } +} + +type BetterObj = Record & object; +const BOXABLE_TYPES = new Set(['boolean', 'number', 'string']); + +// Memory for previously seen containers (object, array, map, set). +// Used for recursion detection, and to avoid repeated comparison. +// +// Elements are { a: val, b: val }. +let memory: { a: unknown; b: unknown }[] = []; + +function useStrictEquality(a: unknown, b: unknown, _strictKeys: boolean) { + return a === b; +} + +function useObjectValueEquality(a: object, b: object) { + return a === b || a.valueOf() === b.valueOf(); +} + +type HasConstructor = { constructor?: unknown }; + +function compareConstructors(a: HasConstructor, b: HasConstructor) { + // Comparing constructors is more strict than using `instanceof` + return getConstructor(a) === getConstructor(b); +} + +function getConstructor(obj: HasConstructor) { + const proto = Object.getPrototypeOf(obj); + + // If the obj prototype descends from a null constructor, treat it + // as a null prototype. + // Ref https://github.com/qunitjs/qunit/issues/851 + // + // Allow objects with no prototype, from Object.create(null), to be equivalent to + // plain objects that have Object as their constructor. + return !proto || proto.constructor === null ? Object : obj.constructor; +} + +function getRegExpFlags(regexp: RegExp) { + // @ts-expect-error never narrowing is only true for modern browsers + return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; +} + +// Specialised comparisons after entryTypeCallbacks.object, based on `objectType()` +const objTypeCallbacks = { + undefined: useStrictEquality, + null: useStrictEquality, + // Handle boxed boolean + boolean: useObjectValueEquality, + number(a: number, b: number) { + // Handle NaN and boxed number + return a === b || a.valueOf() === b.valueOf() || (isNaN(a.valueOf()) && isNaN(b.valueOf())); + }, + // Handle boxed string + string: useObjectValueEquality, + symbol: useStrictEquality, + date: useObjectValueEquality, + + nan() { + return true; + }, + + regexp(a: RegExp, b: RegExp) { + return ( + a.source === b.source && + // Include flags in the comparison + getRegExpFlags(a) === getRegExpFlags(b) + ); + }, + + // identical reference only + function: useStrictEquality, + + array(a: unknown[], b: unknown[], strictKeys: boolean) { + if (a.length !== b.length) { + // Safe and faster + console.log('failed array length check', a, b); + return false; + } + + for (let i = 0; i < a.length; i++) { + if (!typeEquiv(a[i], b[i], strictKeys)) { + console.log('failed array element check', a[i], b[i], a, b); + return false; + } + } + return true; + }, + + // Define sets a and b to be equivalent if for each element aVal in a, there + // is some element bVal in b such that aVal and bVal are equivalent. Element + // repetitions are not counted, so these are equivalent: + // a = new Set( [ X={}, Y=[], Y ] ); + // b = new Set( [ Y, X, X ] ); + set(a: Set, b: Set, strictKeys: boolean) { + if (a.size !== b.size) { + // This optimization has certain quirks because of the lack of + // repetition counting. For instance, adding the same + // (reference-identical) element to two equivalent sets can + // make them non-equivalent. + return false; + } + + let outerEq = true; + + a.forEach((aVal) => { + // Short-circuit if the result is already known. (Using for...of + // with a break clause would be cleaner here, but it would cause + // a syntax error on older JavaScript implementations even if + // Set is unused) + if (!outerEq) { + return; + } + + let innerEq = false; + + b.forEach((bVal) => { + // Likewise, short-circuit if the result is already known + if (innerEq) { + return; + } + + // Swap out the global memory, as nested typeEquiv() would clobber it + const originalMemory = memory; + memory = []; + if (typeEquiv(bVal, aVal, strictKeys)) { + innerEq = true; + } + // Restore + memory = originalMemory; + }); + + if (!innerEq) { + outerEq = false; + } + }); + + return outerEq; + }, + + // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) + // in a, there is some key-value pair (bKey, bVal) in b such that + // [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not + // counted, so these are equivalent: + // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); + // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); + map(a: Map, b: Map, strictKeys: boolean) { + if (a.size !== b.size) { + // This optimization has certain quirks because of the lack of + // repetition counting. For instance, adding the same + // (reference-identical) key-value pair to two equivalent maps + // can make them non-equivalent. + return false; + } + + let outerEq = true; + + a.forEach((aVal, aKey) => { + // Short-circuit if the result is already known. (Using for...of + // with a break clause would be cleaner here, but it would cause + // a syntax error on older JavaScript implementations even if + // Map is unused) + if (!outerEq) { + return; + } + + let innerEq = false; + + b.forEach((bVal, bKey) => { + // Likewise, short-circuit if the result is already known + if (innerEq) { + return; + } + + // Swap out the global memory, as nested typeEquiv() would clobber it + const originalMemory = memory; + memory = []; + if (objTypeCallbacks.array([bVal, bKey], [aVal, aKey], strictKeys)) { + innerEq = true; + } + // Restore + memory = originalMemory; + }); + + if (!innerEq) { + outerEq = false; + } + }); + + return outerEq; + }, +}; + +// Entry points from typeEquiv, based on `typeof` +const entryTypeCallbacks = { + undefined: useStrictEquality, + null: useStrictEquality, + boolean: useStrictEquality, + number(actual: number, expected: number, _strictKeys: boolean) { + // Handle NaN + return actual === expected || (isNaN(actual) && isNaN(expected)); + }, + string: useStrictEquality, + symbol: useStrictEquality, + + function: useStrictEquality, + object(actual: BetterObj, expected: BetterObj, strictKeys: boolean) { + // Handle memory (skip recursion) + if (memory.some((pair) => pair.a === actual && pair.b === expected)) { + return true; + } + memory.push({ a: actual, b: expected }); + + const aObjType = objectType(actual); + const bObjType = objectType(expected); + if (aObjType !== 'object' || bObjType !== 'object') { + // Handle literal `null` + // Handle: Array, Map/Set, Date, Regxp/Function, boxed primitives + // @ts-expect-error + return aObjType === bObjType && objTypeCallbacks[aObjType](actual, expected, strictKeys); + } + + // NOTE: Literal null must not make it here as it would throw + if (strictKeys && compareConstructors(actual, expected) === false) { + return false; + } + + const aProperties = []; + const bProperties = []; + + // Be strict and go deep, no filtering with hasOwnProperty. + if (strictKeys) { + for (const i in actual) { + // Collect a's properties + aProperties.push(i); + + // Skip OOP methods that look the same + if ( + actual.constructor !== Object && + typeof actual.constructor !== 'undefined' && + typeof actual[i] === 'function' && + typeof expected[i] === 'function' && + actual[i].toString() === expected[i].toString() + ) { + continue; + } + if (!typeEquiv(actual[i], expected[i], strictKeys)) { + return false; + } + } + + for (const i in expected) { + // Collect b's properties + bProperties.push(i); + } + + return objTypeCallbacks.array(aProperties.sort(), bProperties.sort(), strictKeys); + } + + for (const i in expected) { + // Collect a's properties + aProperties.push(i); + + // Skip OOP methods that look the same + if ( + expected.constructor !== Object && + typeof expected.constructor !== 'undefined' && + typeof expected[i] === 'function' && + typeof actual[i] === 'function' && + expected[i].toString() === actual[i].toString() + ) { + continue; + } + if (!typeEquiv(actual[i], expected[i], strictKeys)) { + console.log('failed object property check', i, actual[i], expected[i], actual, expected); + return false; + } + } + + return true; + }, +}; + +function typeEquiv(actual: unknown, expected: unknown, strictKeys: boolean): boolean { + // Optimization: Only perform type-specific comparison when pairs are not strictly equal. + if (actual === expected) { + return true; + } + + const aType = typeof actual; + const bType = typeof expected; + if (aType !== bType) { + // Support comparing primitive to boxed primitives + // Try again after possibly unwrapping one + return ( + (aType === 'object' && BOXABLE_TYPES.has(objectType(actual)) ? (actual as string | number).valueOf() : actual) === + (bType === 'object' && BOXABLE_TYPES.has(objectType(expected)) + ? (expected as string | number).valueOf() + : expected) + ); + } + + // @ts-expect-error + return entryTypeCallbacks[aType](actual, expected, strictKeys); +} + +function innerEquiv(actual: unknown, expected: unknown, strictKeys: boolean): boolean { + const res = typeEquiv(actual, expected, strictKeys); + // Release any retained objects and reset recursion detection for next call + memory = []; + return res; +} + +/** + * Test any two types of JavaScript values for equality. + * + * @author Philippe Rathé + * @author David Chan + */ +export default function equiv(actual: unknown, expected: unknown, strictKeys: boolean): boolean { + return actual === expected || innerEquiv(actual, expected, strictKeys); +} diff --git a/packages/diagnostic/src/reporters/dom.ts b/packages/diagnostic/src/reporters/dom.ts new file mode 100644 index 00000000000..94c7608c93e --- /dev/null +++ b/packages/diagnostic/src/reporters/dom.ts @@ -0,0 +1,379 @@ +import type { CompatTestReport, Emitter } from '../-types'; +import type { DiagnosticReport, ModuleReport, Reporter, SuiteReport, TestReport } from '../-types/report'; +import { getSettings, updateConfigValue, updateSuiteState } from '../internals/config'; + +type SuiteLayout = { + report: HTMLElement; + current: HTMLElement; + resultsList: HTMLElement; + results: Map; + cleanup: (() => void)[]; +}; + +export class DOMReporter implements Reporter { + declare element: HTMLElement; + declare settings: ReturnType; + declare suite: SuiteLayout; + declare suiteReport: SuiteReport; + declare currentTests: Map; + declare nextTestId: number; + declare stats: { + diagnostics: number; + diagnosticsPassed: number; + modules: number; + modulesPassed: number; + }; + declare _pendingUpdate: number | null; + declare _socket: Emitter | null; + + constructor(element: HTMLElement, emitter?: Emitter | null) { + this.nextTestId = 1; + this.element = element; + this.settings = getSettings(); + this.stats = { + diagnostics: 0, + diagnosticsPassed: 0, + modules: 0, + modulesPassed: 0, + }; + this._pendingUpdate = null; + this.currentTests = new Map(); + this._socket = emitter || null; + } + + onSuiteStart(report: SuiteReport): void { + if (this.element.children.length) { + this.element.innerHTML = ''; + } + const fragment = document.createDocumentFragment(); + this.suite = renderSuite(fragment, report); + this.element.appendChild(fragment); + this.suiteReport = report; + this._socket?.emit('suite-start', report); + } + + onSuiteFinish(report: SuiteReport): void { + this._socket?.emit('suite-finish', report); + } + + onTestStart(test: TestReport): void { + this.scheduleUpdate(); + if (this._socket) { + const compatTestReport = { + id: this.nextTestId++, + name: test.module.name + ':' + test.name, + items: [], + failed: 0, + passed: 0, + skipped: test.skipped, + todo: test.todo, + total: 0, + runDuration: 0, + testId: test.id, + }; + this.currentTests.set(test.id, compatTestReport); + this._socket.emit('test-start', compatTestReport); + } + } + + onTestFinish(test: TestReport): void { + this.stats.diagnostics += test.result.diagnostics.length; + this.stats.diagnosticsPassed += test.result.diagnostics.filter((d) => d.passed).length; + + if (this._socket) { + const compatTestReport = this.currentTests.get(test.id)!; + console.log(compatTestReport.id, test.name); + this.currentTests.delete(test.id); + compatTestReport.failed += test.result.failed ? 1 : 0; + compatTestReport.passed += test.result.passed ? 1 : 0; + compatTestReport.skipped = test.skipped; + compatTestReport.todo = test.todo; + compatTestReport.total = test.result.diagnostics.length; + compatTestReport.runDuration = test.end!.startTime - test.start!.startTime; + compatTestReport.items = test.result.diagnostics.map((d) => { + // more expensive to serialize the whole diagnostic + if (this.settings.params.debug.value) { + return d; + } + return { + passed: d.passed, + message: d.message, + }; + }); + + if (compatTestReport.failed > 0 || test.result.failed) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.settings.params.debug.value && console.log(test, compatTestReport); + } + + this._socket.emit('test-finish', compatTestReport); + } else if (test.result.failed) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.settings.params.debug.value && console.log(test); + } + + if (this.settings.params.hideReport.value) { + return; + } + // @ts-expect-error + test.moduleName = test.module.name; + this.suite.results.set(test, null); + this.scheduleUpdate(); + } + + onModuleStart(module: ModuleReport): void {} + + onModuleFinish(module: ModuleReport): void { + this.stats.modules++; + this.stats.modulesPassed += module.passed ? 1 : 0; + this.scheduleUpdate(); + } + + onDiagnostic(_diagnostic: DiagnosticReport): void { + this.scheduleUpdate(); + } + + scheduleUpdate() { + if (this._pendingUpdate) { + return; + } + this._pendingUpdate = requestAnimationFrame(() => { + this._pendingUpdate = null; + this._updateRender(); + }); + } + + _updateRender() { + // render infos + // render any tests + let i = 0; + const fragment = document.createDocumentFragment(); + this.suite.results.forEach((element, test) => { + i++; + if (element) { + return; + } + const tr = document.createElement('tr'); + fragment.appendChild(tr); + tr.classList.add(classForTestStatus(test)); + makeRow(tr, [ + i + '.', + `${iconForTestStatus(test)} ${labelForTestStatus(test)}`, + durationForTest(test), + `${test.module.name} > `, + `${test.name} (${test.result.diagnostics.length})`, + getURL(test.id), + ]); + this.suite.results.set(test, tr); + }); + this.suite.resultsList.appendChild(fragment); + } +} + +function makeRow(tr: HTMLTableRowElement, cells: string[]) { + for (let i = 0; i < cells.length; i++) { + const cell = cells[i]; + const td = document.createElement('td'); + if (i === 3) { + const strong = document.createElement('strong'); + const text = document.createTextNode(cell); + strong.appendChild(text); + td.appendChild(strong); + i++; + const text2 = document.createTextNode(cells[i]); + td.appendChild(text2); + } else if (i === 5) { + const a = document.createElement('a'); + a.href = cell; + a.appendChild(document.createTextNode('rerun')); + td.appendChild(a); + } else { + const text = document.createTextNode(cell); + td.appendChild(text); + } + tr.appendChild(td); + } +} + +function getURL(id: string) { + const currentURL = new URL(window.location.href); + currentURL.searchParams.set('t', id); + return currentURL.href; +} + +function durationForTest(test: TestReport) { + if (!test.start || !test.end) { + return 'N/A'; + } + return `${(test.end.startTime - test.start.startTime).toLocaleString('en-US')}ms`; +} + +function labelForTestStatus(test: TestReport) { + if (test.skipped) { + return 'Skip'; + } + if (test.todo && test.result.passed) { + return 'Todo'; + } + if (test.result.passed) { + return 'Pass'; + } + return 'Fail'; +} + +function iconForTestStatus(test: TestReport) { + if (test.skipped) { + return '⚠️'; + } + if (test.todo && test.result.passed) { + return '🛠️'; + } + if (test.result.passed) { + return '✅' + ''; + } + return '💥'; +} + +function classForTestStatus(test: TestReport) { + if (test.skipped) { + return 'skipped'; + } + if (test.todo && test.result.passed) { + return 'todo'; + } + if (test.result.passed) { + return 'passed'; + } + return 'failed'; +} + +function renderSuite(element: DocumentFragment, suiteReport: SuiteReport): SuiteLayout { + const cleanup: (() => void)[] = []; + + // ==== Create the Header Section + const header = document.createElement('header'); + header.id = 'warp-drive__diagnostic-header'; + element.appendChild(header); + + const title = document.createElement('h1'); + title.innerHTML = `@warp-drive/diagnostic`; + header.appendChild(title); + + const paramsList = document.createElement('ul'); + header.appendChild(paramsList); + + const params = getSettings().params; + type Params = keyof typeof params; + const keys = Object.keys(params) as Params[]; + keys.forEach((key) => { + const value = params[key]; + const param = document.createElement('li'); + paramsList.appendChild(param); + const label = document.createElement('label'); + param.appendChild(label); + + const input = document.createElement('input'); + input.id = value.id; + input.name = value.id; + + if (typeof value.value === 'string') { + input.type = 'text'; + input.value = value.value; + } else { + input.type = 'checkbox'; + input.checked = value.value; + } + + function update() { + value.value = input.checked; + updateConfigValue(key, value.value); + } + + input.addEventListener('change', update); + cleanup.push(() => input.removeEventListener('change', update)); + + label.appendChild(input); + label.appendChild(document.createTextNode(` ${value.label || value.id}`)); + }); + + // ==== Create the Controls Section + const controls = document.createElement('div'); + controls.id = 'warp-drive__diagnostic-controls'; + element.appendChild(controls); + + function runPrev() { + updateSuiteState(2); + } + function runNext() { + updateSuiteState(3); + } + function runRestart() { + updateSuiteState(4); + } + + const prevButton = el('button', 'prev'); + prevButton.innerText = 'Prev ⏪️'; + prevButton.addEventListener('click', runPrev); + cleanup.push(() => prevButton.removeEventListener('click', runPrev)); + controls.appendChild(prevButton); + + const nextButton = el('button', 'next'); + nextButton.innerText = 'Next ⏩️'; + nextButton.addEventListener('click', runNext); + cleanup.push(() => nextButton.removeEventListener('click', runNext)); + controls.appendChild(nextButton); + + let isRunning = false; + const runButton = el('button', 'pauseResume'); + runButton.innerText = 'Run ▶️'; + + function updateRunState() { + isRunning = !isRunning; + updateSuiteState(isRunning ? 1 : 0); + if (isRunning) { + runButton.innerText = 'Pause ⏸️'; + } else { + runButton.innerText = 'Run ▶️'; + } + } + runButton.addEventListener('click', updateRunState); + cleanup.push(() => runButton.removeEventListener('click', updateRunState)); + controls.appendChild(runButton); + + const restartButton = el('button', 'restart'); + restartButton.innerText = 'Restart 🔄'; + restartButton.addEventListener('click', runRestart); + cleanup.push(() => restartButton.removeEventListener('click', runRestart)); + controls.appendChild(restartButton); + + // ==== Create the Report Section + const report = document.createElement('div'); + report.id = 'warp-drive__diagnostic-report'; + element.appendChild(report); + + const current = document.createElement('div'); + current.classList.add('current-diagnostic'); + element.appendChild(current); + + const resultsTable = document.createElement('table'); + element.appendChild(resultsTable); + + const resultsList = document.createElement('tbody'); + resultsList.classList.add('diagnostic-results'); + resultsTable.appendChild(resultsList); + + const results = new Map(); + + return { cleanup, report, current, resultsList, results }; +} + +function el(tag: 'button', name: string): HTMLButtonElement; +function el(tag: 'div', name: string): HTMLDivElement; +function el(tag: 'div' | 'button', name: string) { + const element = document.createElement(tag); + element.id = `warp-drive__diagnostic-${name}`; + if (tag === 'button') { + (element as HTMLButtonElement).type = 'button'; + } + return element; +} diff --git a/packages/diagnostic/src/runners/dom.ts b/packages/diagnostic/src/runners/dom.ts new file mode 100644 index 00000000000..02a169668b4 --- /dev/null +++ b/packages/diagnostic/src/runners/dom.ts @@ -0,0 +1,36 @@ +import { registerReporter, start as _start } from '../'; +import type { Emitter } from '../-types'; +import { assert, getGlobal } from '../-utils'; +import type { ConfigOptions } from '../internals/config'; +import { configure, getSettings } from '../internals/config'; +import { DOMReporter } from '../reporters/dom'; + +export async function start(config?: Partial) { + if (config) { + configure(config); + } + + const context = getGlobal(); + const body = context.document?.body; + + assert(`Expected to be in a browser environment`, typeof body !== 'undefined'); + + const container = context.document.getElementById('warp-drive__diagnostic'); + assert( + `Expected to find a diagnostic container element. Make sure your html file has added
`, + container !== null + ); + const settings = getSettings(); + + let emitter: Emitter | null = null; + if (settings.useTestem) { + const { createTestemEmitter } = await import('../emitters/testem'); + emitter = await createTestemEmitter(); + } else if (settings.useDiagnostic) { + const { createDiagnosticEmitter } = await import('../emitters/diagnostic'); + emitter = await createDiagnosticEmitter(); + } + registerReporter(new DOMReporter(container, emitter)); + + await _start(); +} diff --git a/packages/diagnostic/src/styles/dom-reporter.css b/packages/diagnostic/src/styles/dom-reporter.css new file mode 100644 index 00000000000..4795727913d --- /dev/null +++ b/packages/diagnostic/src/styles/dom-reporter.css @@ -0,0 +1,197 @@ +:root { + /* + --heading-color: #17abab; + --heading-2-color: #e617ab; + */ + --brand-pink: hsl(317, 82%, 50%); + --brand-green: hsl(180, 76%, 50%); + --brand-blue: 212, 92%; + --brand-yellow: #deb008; + --brand-main: var(--brand-green); + --brand-secondary: var(--brand-pink); + --primary-color: 212, 92%; + --secondary-color: #536390; + --heading-3-color: #deb008; + --heading-4-color: #0868de; + --heading-2-color: #999; + --font-color: #fff; + --bg-color: #191919; + --bg-color-2: #222222; + --bg-color-3: #292929; + --heading-color: #292922; +} + +html { + font-size: 125%; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: relative; +} + +body { + font-size: 0.75rem; +} + +#warp-drive__diagnostic { + box-sizing: border-box; + color: var(--font-color); + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Oxygen, + Ubuntu, + Cantarell, + "Open Sans", + "Helvetica Neue", + sans-serif; + width: 100%; + position: relative; + min-height: 100%; + line-height: 1.5em; + background-color: var(--bg-color); + /* + background-size: 1.5em 1.5em; + background-image: linear-gradient(to right, var(--bg-color-2) 0.1em, transparent 1px), + linear-gradient(to bottom, var(--bg-color-2) 0.1em, transparent 1px); + */ +} + +#warp-drive__diagnostic h2 { + box-sizing: border-box; + font-size: 1.25em; + font-weight: normal; + color: var(--heading-2-color); + margin: 0; + padding: 0; + line-height: 1.25rem; + margin: 0.25rem 0 1rem; +} + +#warp-drive__diagnostic h1 { + box-sizing: border-box; + font-size: 1.5em; + font-weight: normal; + color: var(--heading-color); + margin: 0; + padding: 0; + line-height: 3rem; + float: left; + display: block; +} + +#warp-drive__diagnostic-header ul { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0; + position: relative; + float: right; + top: 0; + width: calc(100% - 250px); +} + +#warp-drive__diagnostic-header ul li { + box-sizing: border-box; + font-size: 0.85em; + float: right; + margin: 0.25em 0.5em; +} + +#warp-drive__diagnostic-header { + min-width: 800px; + box-sizing: border-box; + height: 4.5rem; + padding: 0.5rem; + background: var(--bg-color-3); + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); + border-bottom: 0.1rem solid hsl(var(--brand-blue), 50%); +} + +#warp-drive__diagnostic h1 span.logo-main { + color: var(--brand-main); + font-weight: 500; +} + +#warp-drive__diagnostic h1 span.logo-pink { + color: var(--brand-secondary); + font-weight: 300; +} + +#warp-drive__diagnostic .text-blue { + color: var(--primary-color); +} + +#warp-drive__diagnostic table { + box-sizing: border-box; + border-collapse: separate; + border-spacing: 0 0.1rem; + width: 100%; + padding: 1rem; +} + +#warp-drive__diagnostic div, +#warp-drive__diagnostic table td { + box-sizing: border-box; +} + +#warp-drive__diagnostic table tr { + box-sizing: border-box; + color: lightgrey; + font-size: 0.85em; + line-height: 1rem; + text-align: left; + vertical-align: middle; + height: 1.2rem; + padding: 0.1rem; +} + +#warp-drive__diagnostic table tr td strong { + box-sizing: border-box; + color: gray; +} + +#warp-drive__diagnostic table tr td a { + box-sizing: border-box; + color: lightblue; +} + +#warp-drive__diagnostic table tr td:first-of-type { + border-radius: 0.1rem 0 0 0.1rem; + padding-left: 0.5rem; +} +#warp-drive__diagnostic table tr td:last-of-type { + padding-right: 0.5rem; + border-radius: 0 0.1rem 0.1rem 0; +} +#warp-drive__diagnostic table tr:nth-last-of-type(even) { + background: rgba(0, 0, 0, 0.8); +} +#warp-drive__diagnostic table tr:nth-last-of-type(odd) { + background: rgba(0, 0, 0, 0.8); +} +#warp-drive__diagnostic table tr.passed td:nth-child(2) { + color: green; +} +#warp-drive__diagnostic table tr.skipped td:nth-child(2) { + color: yellow; +} +#warp-drive__diagnostic table tr.todo td:nth-child(2) { + color: cyan; +} +#warp-drive__diagnostic table tr.failed td:nth-child(2) { + color: red; +} +#warp-drive__diagnostic table td:nth-child(3) { + color: yellow; + font-size: 0.75em; + line-height: 0.75rem; +} diff --git a/packages/diagnostic/test/diagnostic.js b/packages/diagnostic/test/diagnostic.js new file mode 100644 index 00000000000..186b0f50765 --- /dev/null +++ b/packages/diagnostic/test/diagnostic.js @@ -0,0 +1,3 @@ +import launch from '../server/default-setup'; + +await launch(); diff --git a/packages/diagnostic/test/example-tests.js b/packages/diagnostic/test/example-tests.js new file mode 100644 index 00000000000..8a89e3e5f9c --- /dev/null +++ b/packages/diagnostic/test/example-tests.js @@ -0,0 +1,21 @@ +import { module, test } from './@warp-drive/diagnostic/index.js'; +import { start } from './@warp-drive/diagnostic/runners/dom.js'; + +module('example-tests', function () { + test('An example test', function (assert) { + assert.ok(true, 'We ran a test'); + }); + + test('Another example test 2', function (assert) { + assert.ok(false, 'We ran another test 2'); + assert.ok(true, 'We passed!'); + }); + + test('Another example test 3', function (assert) { + assert.ok(true, 'We ran another test 3'); + }); +}); + +start({ + useDiagnostic: true, +}); diff --git a/packages/diagnostic/test/index.html b/packages/diagnostic/test/index.html new file mode 100644 index 00000000000..c908ac9a3f5 --- /dev/null +++ b/packages/diagnostic/test/index.html @@ -0,0 +1,12 @@ + + + + Diagnostic Test + + + +
+
+ + + diff --git a/packages/diagnostic/tsconfig.json b/packages/diagnostic/tsconfig.json new file mode 100644 index 00000000000..0ab91e9bdab --- /dev/null +++ b/packages/diagnostic/tsconfig.json @@ -0,0 +1,39 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../build-config" + } + ] +} diff --git a/packages/diagnostic/vite.config.mjs b/packages/diagnostic/vite.config.mjs new file mode 100644 index 00000000000..032e000eb20 --- /dev/null +++ b/packages/diagnostic/vite.config.mjs @@ -0,0 +1,25 @@ +import { keepAssets } from '@warp-drive/internal-config/vite/keep-assets'; +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember/runloop', + '@ember/test-helpers', + 'ember-cli-test-loader/test-support/index', + '@glimmer/manager', +]; +export const entryPoints = [ + './src/index.ts', + './src/reporters/dom.ts', + './src/runners/dom.ts', + './src/ember.ts', + './src/-types.ts', +]; + +export default createConfig( + { + entryPoints, + externals, + plugins: [keepAssets({ from: 'src', include: ['./styles/**/*.css'], dist: 'dist' })], + }, + import.meta.resolve +); diff --git a/packages/graph/.npmignore b/packages/graph/.npmignore deleted file mode 100644 index e4bce62a5ec..00000000000 --- a/packages/graph/.npmignore +++ /dev/null @@ -1,40 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/node-tests - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/graph/CHANGELOG.md b/packages/graph/CHANGELOG.md new file mode 100644 index 00000000000..6a9b93c99b5 --- /dev/null +++ b/packages/graph/CHANGELOG.md @@ -0,0 +1,74 @@ +# @ember-data/graph Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@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)) + +#### :bug: Bug Fix + +* [#9364](https://github.com/emberjs/data/pull/9364) fix: restore old behavior in deprecation ([@enspandi](https://github.com/enspandi)) +* [#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)) +* [#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 + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) +* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@runspired](https://github.com/runspired)) + +#### Committers: (3) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Andreas Minnich ([@enspandi](https://github.com/enspandi)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) +* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@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)) + +#### :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)) +* [#9097](https://github.com/emberjs/data/pull/9097) fix: allow decorator syntax in code comments during yui doc processing ([@jaredgalanis](https://github.com/jaredgalanis)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) +* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (3) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Jared Galanis ([@jaredgalanis](https://github.com/jaredgalanis)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/graph/README.md b/packages/graph/README.md index a3a84f56aef..f381b4f1ab7 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -28,3 +28,11 @@ Install using your javascript package manager of choice. For instance with [pnpm ```no-highlight pnpm add @ember-data/graph ``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/graph/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/graph/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/graph/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/graph/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/graph/lts-4-12?label=%40lts-4-12&color=bbbbbb) diff --git a/packages/graph/addon-main.cjs b/packages/graph/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/graph/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/graph/addon-main.js b/packages/graph/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/graph/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/graph/babel.config.js b/packages/graph/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/graph/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/graph/babel.config.mjs b/packages/graph/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/graph/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/graph/ember-data-logo-dark.svg b/packages/graph/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/graph/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/graph/ember-data-logo-light.svg b/packages/graph/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/graph/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/graph/eslint.config.mjs b/packages/graph/eslint.config.mjs new file mode 100644 index 00000000000..c42683457ca --- /dev/null +++ b/packages/graph/eslint.config.mjs @@ -0,0 +1,23 @@ +// @ts-check +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 [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: externals, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/graph/package.json b/packages/graph/package.json index 64ce91f6bc4..90e8d04c9d0 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -11,70 +11,91 @@ "directory": "packages/graph" }, "license": "MIT", - "author": "", + "author": "Chris Thoburn ", "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" - }, - "ember-addon": { - "main": "addon-main.js", - "type": "addon", - "version": 1 + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], - "peerDependencies": { - "@ember-data/store": "workspace:4.12.8" + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@ember-data/store": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/store": "workspace:*", + "@warp-drive/core-types": "workspace:*" + }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "ember-cli-babel": "^7.26.11" + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "typescript": "^5.0.3", - "walk-sync": "^3.0.0", - "webpack": "^5.77.0" - }, - "ember": { - "edition": "octane" + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 + }, + "ember": { + "edition": "octane" + } } diff --git a/packages/graph/rollup.config.mjs b/packages/graph/rollup.config.mjs deleted file mode 100644 index a5afe47ec93..00000000000 --- a/packages/graph/rollup.config.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@ember-data/store/-private', - '@ember/service', - 'ember-inflector', - '@ember/debug', - '@ember/string', - '@ember/object', - '@ember/object/mixin', - '@ember/application', - '@glimmer/env', - '@ember/polyfills', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'error.js', 'json-api.js', 'rest.js', '-private.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/graph/src/-private.ts b/packages/graph/src/-private.ts index 1869f9a21ac..5b0b2d75d87 100644 --- a/packages/graph/src/-private.ts +++ b/packages/graph/src/-private.ts @@ -1,5 +1,3 @@ -export { graphFor, peekGraph } from './-private/graph/index'; - /** *

( + newState: T[], + newMembers: Set, + prevState: T[], + prevSet: Set, + onAdd: (v: T) => void, + onDel: (v: T) => void +): { duplicates: Map; diff: Diff } { + const newLength = newState.length; + const prevLength = prevState.length; + const iterationLength = Math.max(newLength, prevLength); + let changed: boolean = newMembers.size !== prevSet.size; + const added = new Set(); + const removed = new Set(); + const duplicates = new Map(); + const finalSet = new Set(); + const finalState: T[] = []; + + for (let i = 0, j = 0; i < iterationLength; i++) { + let adv = false; + let member: T | undefined; + + // accumulate anything added + if (i < newLength) { + member = newState[i]; + + if (!finalSet.has(member)) { + finalState[j] = member; + finalSet.add(member); + adv = true; + + if (!prevSet.has(member)) { + changed = true; + added.add(member); + onAdd(member); + } + } else { + let list = duplicates.get(member); + + if (list === undefined) { + list = []; + duplicates.set(member, list); + } + + list.push(i); + } + } + + // accumulate anything removed + if (i < prevLength) { + const prevMember = prevState[i]; + + // detect reordering, adjusting index for duplicates + // j is always less than i and so if i < prevLength, j < prevLength + if (member !== prevState[j]) { + changed = true; + } + + if (!newMembers.has(prevMember)) { + changed = true; + removed.add(prevMember); + onDel(prevMember); + } + } else if (adv && j < prevLength && member !== prevState[j]) { + changed = true; + } + + if (adv) { + j++; + } + } + + const diff = { + add: added, + del: removed, + finalState, + finalSet, + changed, + }; + + return { + diff, + duplicates, + }; +} + +function _compare( + finalState: T[], + finalSet: Set, + prevState: T[], + prevSet: Set, + onAdd: (v: T) => void, + onDel: (v: T) => void +): Diff { + const finalLength = finalState.length; + const prevLength = prevState.length; + const iterationLength = Math.max(finalLength, prevLength); + const equalLength = finalLength === prevLength; + let changed: boolean = finalSet.size !== prevSet.size; + const added = new Set(); + const removed = new Set(); + + for (let i = 0; i < iterationLength; i++) { + let member: T | undefined; + + // accumulate anything added + if (i < finalLength) { + member = finalState[i]; + if (!prevSet.has(member)) { + changed = true; + added.add(member); + onAdd(member); + } + } + + // accumulate anything removed + if (i < prevLength) { + const prevMember = prevState[i]; + + // detect reordering + if (equalLength && member !== prevMember) { + changed = true; + } + + if (!finalSet.has(prevMember)) { + changed = true; + removed.add(prevMember); + onDel(prevMember); + } + } + } + + return { + add: added, + del: removed, + finalState, + finalSet, + changed, + }; +} + +type Diff = { + add: Set; + del: Set; + finalState: T[]; + finalSet: Set; + changed: boolean; +}; + +export function diffCollection( + finalState: StableRecordIdentifier[], + relationship: CollectionEdge, + onAdd: (v: StableRecordIdentifier) => void, + onDel: (v: StableRecordIdentifier) => void +): Diff { + const finalSet = new Set(finalState); + const { remoteState, remoteMembers } = relationship; + + if (DEPRECATE_NON_UNIQUE_PAYLOADS) { + if (finalState.length !== finalSet.size) { + 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`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + for: 'ember-data', + until: '6.0', + since: { available: '4.13', enabled: '5.3' }, + } + ); + // eslint-disable-next-line no-console + console.log(duplicates); + } + + return diff; + } + } else { + assert( + `Expected all entries in the relationship to be unique, found duplicates`, + finalState.length === finalSet.size + ); + } + + return _compare(finalState, finalSet, remoteState, remoteMembers, onAdd, onDel); +} + +export function computeLocalState(storage: CollectionEdge): StableRecordIdentifier[] { + if (!storage.isDirty) { + assert(`Expected localState to be present`, Array.isArray(storage.localState)); + return storage.localState; + } + + const state = storage.remoteState.slice(); + + storage.removals?.forEach((v) => { + const index = state.indexOf(v); + state.splice(index, 1); + }); + + storage.additions?.forEach((v) => { + state.push(v); + }); + storage.localState = state; + storage.isDirty = false; + + return state; +} + +export function _addLocal( + graph: Graph, + record: StableRecordIdentifier, + relationship: CollectionEdge, + value: StableRecordIdentifier, + index: number | null +): boolean { + const { remoteMembers, removals } = relationship; + let additions = relationship.additions; + const hasPresence = remoteMembers.has(value) || additions?.has(value); + + if (hasPresence && !removals?.has(value)) { + assert( + `Attempted to add the resource '${value.lid}' to the collection <${relationship.identifier.type}>.${relationship.definition.key} it was already in`, + hasPresence && !removals?.has(value) + ); + return false; + } + + if (removals?.has(value)) { + removals.delete(value); + } else { + if (!additions) { + additions = relationship.additions = new Set(); + } + + relationship.state.hasReceivedData = true; + additions.add(value); + + const { type } = relationship.definition; + if (type !== value.type) { + if (DEBUG) { + assertPolymorphicType(record, relationship.definition, value, graph.store); + } + graph.registerPolymorphicType(value.type, type); + } + } + + // if we have existing localState + // and we have an index + // apply the change, as this is more efficient + // than recomputing localState and + // it allows us to preserve local ordering + // to a small extend. Local ordering should not + // be relied upon as any remote change will blow it away + if (relationship.localState) { + if (index !== null) { + relationship.localState.splice(index, 0, value); + } else { + relationship.localState.push(value); + } + } + assert( + `Expected relationship to be dirty when adding a local mutation`, + relationship.localState || relationship.isDirty + ); + + return true; +} + +export function _removeLocal(relationship: CollectionEdge, value: StableRecordIdentifier): boolean { + assert(`expected an identifier to remove from the collection relationship`, value); + const { remoteMembers, additions } = relationship; + let removals = relationship.removals; + const hasPresence = remoteMembers.has(value) || additions?.has(value); + + if (!hasPresence || removals?.has(value)) { + assert( + `Attempted to remove the resource '${value.lid}' from the collection <${relationship.identifier.type}>.${relationship.definition.key} but it was not present`, + !hasPresence || removals?.has(value) + ); + return false; + } + + if (additions?.has(value)) { + additions.delete(value); + } else { + if (!removals) { + removals = relationship.removals = new Set(); + } + + removals.add(value); + } + + // if we have existing localState + // apply the change, as this is more efficient + // than recomputing localState and + // it allows us to preserve local ordering + // to a small extend. Local ordering should not + // be relied upon as any remote change will blow it away + if (relationship.localState) { + const index = relationship.localState.indexOf(value); + assert(`Cannot remove a resource that is not present`, index !== -1); + relationship.localState.splice(index, 1); + } + assert( + `Expected relationship to be dirty when performing a local mutation`, + relationship.localState || relationship.isDirty + ); + + return true; +} + +export function _removeRemote(relationship: CollectionEdge, value: StableRecordIdentifier): boolean { + assert(`expected an identifier to remove from the collection relationship`, value); + const { remoteMembers, additions, removals, remoteState } = relationship; + + assert(`Cannot remove a resource that is not present`, remoteMembers.has(value)); + if (!remoteMembers.has(value)) { + return false; + } + + // remove from remote state + remoteMembers.delete(value); + let index = remoteState.indexOf(value); + assert(`Cannot remove a resource that is not present`, index !== -1); + remoteState.splice(index, 1); + + // remove from removals if present + if (removals?.has(value)) { + removals.delete(value); + + // nothing more to do this was our state already + return false; + } + + assert( + `Remote state indicated removal of a resource that was present only as a local mutation`, + !additions?.has(value) + ); + + // if we have existing localState + // and we have an index + // apply the change, as this is more efficient + // than recomputing localState and + // it allows us to preserve local ordering + // to a small extend. Local ordering should not + // be relied upon as any remote change will blow it away + if (relationship.localState) { + index = relationship.localState.indexOf(value); + assert(`Cannot remove a resource that is not present`, index !== -1); + relationship.localState.splice(index, 1); + } + assert( + `Expected relationship to be dirty when performing a local mutation`, + relationship.localState || relationship.isDirty + ); + + return true; +} + +export function rollbackRelationship( + graph: Graph, + identifier: StableRecordIdentifier, + field: string, + relationship: CollectionEdge | ResourceEdge +): void { + if (isBelongsTo(relationship)) { + replaceRelatedRecord( + graph, + { + op: 'replaceRelatedRecord', + record: identifier, + field, + value: relationship.remoteState, + }, + false + ); + } else { + replaceRelatedRecords( + graph, + { + op: 'replaceRelatedRecords', + record: identifier, + field, + value: relationship.remoteState.slice(), + }, + false + ); + } +} diff --git a/packages/graph/src/-private/-edge-definition.ts b/packages/graph/src/-private/-edge-definition.ts new file mode 100644 index 00000000000..a6868f487db --- /dev/null +++ b/packages/graph/src/-private/-edge-definition.ts @@ -0,0 +1,613 @@ +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'; +import type { + CollectionField, + FieldSchema, + LegacyBelongsToField, + LegacyHasManyField, + ResourceField, +} from '@warp-drive/core-types/schema/fields'; + +import { expandingGet, expandingSet, getStore } from './-utils'; +import { assertInheritedSchema } from './debug/assert-polymorphic-type'; +import type { Graph } from './graph'; + +export type EdgeCache = Record>; + +export type RelationshipField = LegacyBelongsToField | LegacyHasManyField | ResourceField | CollectionField; +export type RelationshipFieldKind = RelationshipField['kind']; +export type CollectionKind = 'hasMany' | 'collection'; +export type ResourceKind = 'belongsTo' | 'resource'; +export const RELATIONSHIP_KINDS = ['belongsTo', 'hasMany', 'resource', 'collection']; + +export function isLegacyField(field: FieldSchema): field is LegacyBelongsToField | LegacyHasManyField { + return field.kind === 'belongsTo' || field.kind === 'hasMany'; +} + +export function isRelationshipField(field: FieldSchema): field is RelationshipField { + return RELATIONSHIP_KINDS.includes(field.kind); +} + +export function temporaryConvertToLegacy( + field: ResourceField | CollectionField +): LegacyBelongsToField | LegacyHasManyField { + return { + kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', + name: field.name, + type: field.type, + options: Object.assign({}, { async: false, inverse: null, resetOnRemoteUpdate: false as const }, field.options), + }; +} + +/** + * + * Given RHS (Right Hand Side) + * + * ```ts + * class User extends Model { + * @hasMany('animal', { async: false, inverse: 'owner' }) pets; + * } + * ``` + * + * Given LHS (Left Hand Side) + * + * ```ts + * class Animal extends Model { + * @belongsTo('user', { async: false, inverse: 'pets' }) owner; + * } + * ``` + * + * The UpgradedMeta for the RHS would be: + * + * ```ts + * { + * kind: 'hasMany', + * key: 'pets', + * type: 'animal', + * isAsync: false, + * isImplicit: false, + * isCollection: true, + * isPolymorphic: false, + * inverseKind: 'belongsTo', + * inverseKey: 'owner', + * inverseType: 'user', + * inverseIsAsync: false, + * inverseIsImplicit: false, + * inverseIsCollection: false, + * inverseIsPolymorphic: false, + * } + * ``` + * + * The UpgradeMeta for the LHS would be: + * + * ```ts + * { + * kind: 'belongsTo', + * key: 'owner', + * type: 'user', + * isAsync: false, + * isImplicit: false, + * isCollection: false, + * isPolymorphic: false, + * inverseKind: 'hasMany', + * inverseKey: 'pets', + * inverseType: 'animal', + * inverseIsAsync: false, + * inverseIsImplicit: false, + * inverseIsCollection: true, + * inverseIsPolymorphic: false, + * } + * ``` + * + * + * @class UpgradedMeta + * @internal + */ +export interface UpgradedMeta { + kind: 'implicit' | RelationshipFieldKind; + /** + * The field name on `this` record + * + * @internal + */ + key: string; + /** + * The `type` of the related record + * + * @internal + */ + type: string; + isAsync: boolean; + isImplicit: boolean; + isCollection: boolean; + isPolymorphic: boolean; + resetOnRemoteUpdate: boolean; + + inverseKind: 'implicit' | RelationshipFieldKind; + /** + * The field name on the opposing record + * @internal + */ + inverseKey: string; + /** + * The `type` of `this` record + * @internal + */ + inverseType: string; + inverseIsAsync: boolean; + inverseIsImplicit: boolean; + inverseIsCollection: boolean; + inverseIsPolymorphic: boolean; +} + +export interface EdgeDefinition { + lhs_key: string; + lhs_modelNames: string[]; + lhs_baseModelName: string; + lhs_relationshipName: string; + lhs_definition: UpgradedMeta; + lhs_isPolymorphic: boolean; + + rhs_key: string; + rhs_modelNames: string[]; + rhs_baseModelName: string; + rhs_relationshipName: string; + rhs_definition: UpgradedMeta | null; + rhs_isPolymorphic: boolean; + + hasInverse: boolean; + + /** + * Whether this relationship points back at the same type. + * + * If the relationship is polymorphic, this will be true if + * it points back at the same abstract type. + * + * @internal + */ + isSelfReferential: boolean; + + /** + * If this is a reflexive relationship, this is true + * if the relationship also points back at the same + * field. + * + * @internal + */ + isReflexive: boolean; +} + +const BOOL_LATER = null as unknown as boolean; +const STR_LATER = ''; +const IMPLICIT_KEY_RAND = Date.now(); + +function implicitKeyFor(type: string, key: string): string { + return `implicit-${type}:${key}${IMPLICIT_KEY_RAND}`; +} + +function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { + definition.inverseKind = inverseDefinition.kind; + definition.inverseKey = inverseDefinition.key; + definition.inverseType = inverseDefinition.type; + definition.inverseIsAsync = inverseDefinition.isAsync; + definition.inverseIsCollection = inverseDefinition.isCollection; + definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic; + definition.inverseIsImplicit = inverseDefinition.isImplicit; + const resetOnRemoteUpdate = + definition.resetOnRemoteUpdate === false || inverseDefinition.resetOnRemoteUpdate === false ? false : true; + definition.resetOnRemoteUpdate = resetOnRemoteUpdate; + inverseDefinition.resetOnRemoteUpdate = resetOnRemoteUpdate; +} + +function upgradeMeta(meta: RelationshipField): UpgradedMeta { + if (!isLegacyField(meta)) { + meta = temporaryConvertToLegacy(meta); + } + const niceMeta: UpgradedMeta = {} as UpgradedMeta; + const options = meta.options; + niceMeta.kind = meta.kind; + niceMeta.key = meta.name; + niceMeta.type = meta.type; + assert(`Expected relationship definition to specify async`, typeof options?.async === 'boolean'); + niceMeta.isAsync = options.async; + niceMeta.isImplicit = false; + niceMeta.isCollection = meta.kind === 'hasMany'; + niceMeta.isPolymorphic = options && !!options.polymorphic; + + 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.resetOnRemoteUpdate = isLegacyField(meta) + ? meta.options?.resetOnRemoteUpdate === false + ? false + : true + : false; + + return niceMeta; +} + +function assertConfiguration(info: EdgeDefinition, type: string, key: string) { + if (DEBUG) { + const isSelfReferential = info.isSelfReferential; + + if (isSelfReferential) { + return true; + } + + const _isRHS = + key === info.rhs_relationshipName && + (type === info.rhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.lhs_isPolymorphic && info.rhs_modelNames.includes(type))); // polymorphic + const _isLHS = + key === info.lhs_relationshipName && + (type === info.lhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.rhs_isPolymorphic && info.lhs_modelNames.includes(type))); // polymorphic; + + if (!_isRHS && !_isLHS) { + /* + this occurs when we are likely polymorphic but not configured to be polymorphic + most often due to extending a class that has a relationship definition on it. + + e.g. + + ```ts + class Pet extends Model { + @belongsTo('human', { async: false, inverse: 'pet' }) owner; + } + class Human extends Model { + @belongsTo('pet', { async: false, inverse: 'owner' }) pet; + } + class Farmer extends Human {} + ``` + + In the above case, the following would trigger this error: + + ```ts + let pet = store.createRecord('pet'); + let farmer = store.createRecord('farmer'); + farmer.pet = pet; // error + ``` + + The correct way to fix this is to specify the polymorphic option on Pet + and to specify the abstract type 'human' on the Human base class. + + ```ts + class Pet extends Model { + @belongsTo('human', { async: false, inverse: 'pet', polymorphic: true }) owner; + } + class Human extends Model { + @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; + } + class Farmer extends Human {} + ``` + + Alternatively both Human and Farmer could declare the relationship, because relationship + definitions are "structural". + + ```ts + class Pet extends Model { + @belongsTo('human', { async: false, inverse: 'pet', polymorphic: true }) owner; + } + class Human extends Model { + @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; + } + class Farmer extends Model { + @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; + } + ``` + + */ + if (key === info.lhs_relationshipName && info.lhs_modelNames.includes(type)) { + // parentIdentifier, parentDefinition, addedIdentifier, store + assertInheritedSchema(info.lhs_definition, type); + } else if (key === info.rhs_relationshipName && info.rhs_modelNames.includes(type)) { + assertInheritedSchema(info.lhs_definition, type); + } + // OPEN AN ISSUE :: we would like to improve our errors but need to understand what corner case got us here + throw new Error( + `PLEASE OPEN AN ISSUE :: Found a relationship that is neither the LHS nor RHS of the same edge. This is not supported. Please report this to the EmberData team.` + ); + } + + if (_isRHS && _isLHS) { + // not sure how we get here but it's probably the result of some form of inheritance + // without having specified polymorphism correctly leading to it not being self-referential + // OPEN AN ISSUE :: we would like to improve our errors but need to understand what corner case got us here + throw new Error( + `PLEASE OPEN AN ISSUE :: Found a relationship that is both the LHS and RHS of the same edge but is not self-referential. This is not supported. Please report this to the EmberData team.` + ); + } + } +} + +export function isLHS(info: EdgeDefinition, type: string, key: string): boolean { + const isSelfReferential = info.isSelfReferential; + const isRelationship = key === info.lhs_relationshipName; + + if (DEBUG) { + assertConfiguration(info, type, key); + } + + if (isRelationship === true) { + return ( + isSelfReferential === true || // itself + type === info.lhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.rhs_isPolymorphic && info.lhs_modelNames.includes(type)) // polymorphic + ); + } + + return false; +} + +export function isRHS(info: EdgeDefinition, type: string, key: string): boolean { + const isSelfReferential = info.isSelfReferential; + const isRelationship = key === info.rhs_relationshipName; + + if (DEBUG) { + assertConfiguration(info, type, key); + } + + if (isRelationship === true) { + return ( + isSelfReferential === true || // itself + type === info.rhs_baseModelName || // base or non-polymorphic + // if the other side is polymorphic then we need to scan our modelNames + (info.lhs_isPolymorphic && info.rhs_modelNames.includes(type)) // polymorphic + ); + } + + return false; +} + +export function upgradeDefinition( + graph: Graph, + identifier: StableRecordIdentifier, + propertyName: string, + isImplicit = false +): EdgeDefinition | null { + const cache = graph._definitionCache; + const storeWrapper = graph.store; + const polymorphicLookup = graph._potentialPolymorphicTypes; + + const { type } = identifier; + let cached = /*#__NOINLINE__*/ expandingGet(cache, type, propertyName); + + // CASE: We have a cached resolution (null if no relationship exists) + if (cached !== undefined) { + return cached; + } + + assert( + `Expected to find relationship definition in the cache for the implicit relationship ${propertyName}`, + !isImplicit + ); + + const relationships = storeWrapper.schema.fields(identifier); + assert(`Expected to have a relationship definition for ${type} but none was found.`, relationships); + const meta = relationships.get(propertyName); + + if (!meta) { + // TODO potentially we should just be permissive here since this is an implicit relationship + // and not require the lookup table to be populated + if (polymorphicLookup[type]) { + const altTypes = Object.keys(polymorphicLookup[type]); + for (let i = 0; i < altTypes.length; i++) { + const _cached = expandingGet(cache, altTypes[i], propertyName); + if (_cached) { + /*#__NOINLINE__*/ expandingSet(cache, type, propertyName, _cached); + _cached.rhs_modelNames.push(type); + return _cached; + } + } + } + + // CASE: We don't have a relationship at all + // we should only hit this in prod + assert(`Expected a relationship schema for '${type}.${propertyName}', but no relationship schema was found.`, meta); + + cache[type][propertyName] = null; + return null; + } + + assert(`Expected ${propertyName} to be a relationship`, isRelationshipField(meta)); + const definition = /*#__NOINLINE__*/ upgradeMeta(meta); + + let inverseDefinition: UpgradedMeta | null; + let inverseKey: string | null; + const inverseType = definition.type; + + // CASE: Inverse is explicitly null + if (definition.inverseKey === null) { + // TODO probably dont need this assertion if polymorphic + assert(`Expected the inverse model to exist`, getStore(storeWrapper).modelFor(inverseType)); + inverseDefinition = null; + } else { + inverseKey = /*#__NOINLINE__*/ inverseForRelationship(getStore(storeWrapper), identifier, propertyName); + + // CASE: If we are polymorphic, and we declared an inverse that is non-null + // we must assume that the lack of inverseKey means that there is no + // concrete type as the baseType, so we must construct and artificial + // placeholder + if (!inverseKey && definition.isPolymorphic && definition.inverseKey) { + inverseDefinition = { + kind: 'belongsTo', // this must be updated when we find the first belongsTo or hasMany definition that matches + key: definition.inverseKey, + type: type, + isAsync: false, // this must be updated when we find the first belongsTo or hasMany definition that matches + isImplicit: false, + isCollection: false, // this must be updated when we find the first belongsTo or hasMany definition that matches + isPolymorphic: false, + } as UpgradedMeta; // the rest of the fields are populated by syncMeta + + // CASE: Inverse resolves to null + } else if (!inverseKey) { + inverseDefinition = null; + } else { + // CASE: We have an explicit inverse or were able to resolve one + const inverseDefinitions = storeWrapper.schema.fields({ type: inverseType }); + assert(`Expected to have a relationship definition for ${inverseType} but none was found.`, inverseDefinitions); + const metaFromInverse = inverseDefinitions.get(inverseKey); + assert( + `Expected a relationship schema for '${inverseType}.${inverseKey}' to match the inverse of '${type}.${propertyName}', but no relationship schema was found.`, + metaFromInverse + ); + assert(`Expected ${inverseKey} to be a relationship`, isRelationshipField(metaFromInverse)); + + inverseDefinition = upgradeMeta(metaFromInverse); + } + } + + // CASE: We have no inverse + if (!inverseDefinition) { + // polish off meta + inverseKey = /*#__NOINLINE__*/ implicitKeyFor(type, propertyName); + inverseDefinition = { + kind: 'implicit', + key: inverseKey, + type: type, + isAsync: false, + isImplicit: true, + isCollection: true, // with implicits any number of records could point at us + isPolymorphic: false, + } as UpgradedMeta; // the rest of the fields are populated by syncMeta + + syncMeta(definition, inverseDefinition); + syncMeta(inverseDefinition, definition); + + const info = { + lhs_key: `${type}:${propertyName}`, + lhs_modelNames: [type], + lhs_baseModelName: type, + lhs_relationshipName: propertyName, + lhs_definition: definition, + lhs_isPolymorphic: definition.isPolymorphic, + + rhs_key: inverseDefinition.key, + rhs_modelNames: [inverseType], + rhs_baseModelName: inverseType, + rhs_relationshipName: inverseDefinition.key, + rhs_definition: inverseDefinition, + rhs_isPolymorphic: false, + + hasInverse: false, + isSelfReferential: type === inverseType, // this could be wrong if we are self-referential but also polymorphic + isReflexive: false, // we can't be reflexive if we don't define an inverse + }; + + expandingSet(cache, inverseType, inverseKey, info); + expandingSet(cache, type, propertyName, info); + return info; + } + + // CASE: We do have an inverse + const baseType = inverseDefinition.type; + + // TODO we want to assert this but this breaks all of our shoddily written tests + /* + if (DEBUG) { + let inverseDoubleCheck = inverseFor(inverseRelationshipName, store); + + assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck); + } + */ + // CASE: We may have already discovered the inverse for the baseModelName + // CASE: We have already discovered the inverse + assert( + `We should have determined an inverseKey by now, open an issue if this is hit`, + typeof inverseKey! === 'string' && inverseKey.length > 0 + ); + cached = expandingGet(cache, baseType, propertyName) || expandingGet(cache, inverseType, inverseKey); + + if (cached) { + // TODO this assert can be removed if the above assert is enabled + assert( + `The ${inverseType}:${inverseKey} relationship declares 'inverse: null', but it was resolved as the inverse for ${type}:${propertyName}.`, + cached.hasInverse !== false + ); + + const _isLHS = cached.lhs_baseModelName === baseType; + const modelNames = _isLHS ? cached.lhs_modelNames : cached.rhs_modelNames; + // make this lookup easier in the future by caching the key + modelNames.push(type); + expandingSet(cache, type, propertyName, cached); + + return cached; + } + + // this is our first time so polish off the metas + syncMeta(definition, inverseDefinition); + syncMeta(inverseDefinition, definition); + + const lhs_modelNames = [type]; + if (type !== baseType) { + lhs_modelNames.push(baseType); + } + const isSelfReferential = baseType === inverseType; + const info = { + lhs_key: `${baseType}:${propertyName}`, + lhs_modelNames, + lhs_baseModelName: baseType, + lhs_relationshipName: propertyName, + lhs_definition: definition, + lhs_isPolymorphic: definition.isPolymorphic, + + rhs_key: `${inverseType}:${inverseKey}`, + rhs_modelNames: [inverseType], + rhs_baseModelName: inverseType, + rhs_relationshipName: inverseKey, + rhs_definition: inverseDefinition, + rhs_isPolymorphic: inverseDefinition.isPolymorphic, + hasInverse: true, + isSelfReferential, + isReflexive: isSelfReferential && propertyName === inverseKey, + }; + + // Create entries for the baseModelName as well as modelName to speed up + // inverse lookups + expandingSet(cache, baseType, propertyName, info); + expandingSet(cache, type, propertyName, info); + + // Greedily populate the inverse + expandingSet(cache, inverseType, inverseKey, info); + + 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.`, + definition.options?.inverse === null || + (typeof definition.options?.inverse === 'string' && definition.options.inverse.length > 0) + ); + return definition.options.inverse; +} diff --git a/packages/graph/src/-private/graph/-state.ts b/packages/graph/src/-private/-state.ts similarity index 100% rename from packages/graph/src/-private/graph/-state.ts rename to packages/graph/src/-private/-state.ts diff --git a/packages/graph/src/-private/-utils.ts b/packages/graph/src/-private/-utils.ts new file mode 100644 index 00000000000..ccdf4aa740d --- /dev/null +++ b/packages/graph/src/-private/-utils.ts @@ -0,0 +1,247 @@ +import { inspect, warn } from '@ember/debug'; + +import type { Store } from '@ember-data/store/-private'; +import { peekCache } from '@ember-data/store/-private'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import { LOG_GRAPH } from '@warp-drive/build-config/debugging'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { UpdateRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { UpgradedMeta } from './-edge-definition'; +import { coerceId } from './coerce-id'; +import type { CollectionEdge } from './edges/collection'; +import type { ImplicitEdge } from './edges/implicit'; +import type { ResourceEdge } from './edges/resource'; +import type { Graph, GraphEdge } from './graph'; + +export function getStore(wrapper: CacheCapabilitiesManager | { _store: Store }): Store { + assert(`expected a private _store property`, '_store' in wrapper); + return wrapper._store; +} + +export function expandingGet(cache: Record>, key1: string, key2: string): T | undefined { + const mainCache = (cache[key1] = cache[key1] || Object.create(null)); + return mainCache[key2]; +} + +export function expandingSet(cache: Record>, key1: string, key2: string, value: T): void { + const mainCache = (cache[key1] = cache[key1] || Object.create(null)); + mainCache[key2] = value; +} + +export function assertValidRelationshipPayload(graph: Graph, op: UpdateRelationshipOperation) { + const relationship = graph.get(op.record, op.field); + assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); + const payload = op.value; + const { definition, identifier, state } = relationship; + const { type } = identifier; + const { field } = op; + const { isAsync, kind } = definition; + + if (payload.links) { + warn( + `You pushed a record of type '${type}' with a relationship '${field}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, + isAsync || !!payload.data || state.hasReceivedData, + { + id: 'ds.store.push-link-for-sync-relationship', + } + ); + } else if (payload.data) { + if (kind === 'belongsTo') { + assert( + `A ${type} record was pushed into the store with the value of ${field} being ${inspect( + payload.data + )}, but ${field} is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, + !Array.isArray(payload.data) + ); + assertRelationshipData(getStore(graph.store), identifier, payload.data, definition); + } else if (kind === 'hasMany') { + assert( + `A ${type} record was pushed into the store with the value of ${field} being '${inspect( + payload.data + )}', but ${field} is a hasMany relationship so the value must be an array. You should probably check your data payload or serializer.`, + Array.isArray(payload.data) + ); + if (Array.isArray(payload.data)) { + for (let i = 0; i < payload.data.length; i++) { + assertRelationshipData(getStore(graph.store), identifier, payload.data[i], definition); + } + } + } + } +} + +export function isNew(identifier: StableRecordIdentifier): boolean { + if (!identifier.id) { + return true; + } + const cache = peekCache(identifier); + return Boolean(cache?.isNew(identifier)); +} + +export function isBelongsTo(relationship: GraphEdge): relationship is ResourceEdge { + return relationship.definition.kind === 'belongsTo'; +} + +export function isImplicit(relationship: GraphEdge): relationship is ImplicitEdge { + return relationship.definition.isImplicit; +} + +export function isHasMany(relationship: GraphEdge): relationship is CollectionEdge { + return relationship.definition.kind === 'hasMany'; +} + +export function forAllRelatedIdentifiers(rel: GraphEdge, cb: (identifier: StableRecordIdentifier) => void): void { + if (isBelongsTo(rel)) { + if (rel.remoteState) { + cb(rel.remoteState); + } + if (rel.localState && rel.localState !== rel.remoteState) { + cb(rel.localState); + } + } else if (isHasMany(rel)) { + // TODO + // rel.remoteMembers.forEach(cb); + // might be simpler if performance is not a concern + for (let i = 0; i < rel.remoteState.length; i++) { + const inverseIdentifier = rel.remoteState[i]; + cb(inverseIdentifier); + } + rel.additions?.forEach(cb); + } else { + rel.localMembers.forEach(cb); + rel.remoteMembers.forEach((inverseIdentifier) => { + if (!rel.localMembers.has(inverseIdentifier)) { + cb(inverseIdentifier); + } + }); + } +} + +/* + Removes the given identifier from BOTH remote AND local state. + + This method is useful when either a deletion or a rollback on a new record + needs to entirely purge itself from an inverse relationship. + */ +export function removeIdentifierCompletelyFromRelationship( + graph: Graph, + relationship: GraphEdge, + value: StableRecordIdentifier, + silenceNotifications?: boolean +): void { + if (isBelongsTo(relationship)) { + if (relationship.remoteState === value) { + relationship.remoteState = null; + } + + if (relationship.localState === value) { + relationship.localState = null; + // This allows dematerialized inverses to be rematerialized + // we shouldn't be notifying here though, figure out where + // a notification was missed elsewhere. + if (!silenceNotifications) { + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } + } else if (isHasMany(relationship)) { + relationship.remoteMembers.delete(value); + relationship.additions?.delete(value); + const wasInRemovals = relationship.removals?.delete(value); + + const canonicalIndex = relationship.remoteState.indexOf(value); + if (canonicalIndex !== -1) { + relationship.remoteState.splice(canonicalIndex, 1); + } + + if (!wasInRemovals) { + const currentIndex = relationship.localState?.indexOf(value); + if (currentIndex !== -1 && currentIndex !== undefined) { + relationship.localState!.splice(currentIndex, 1); + // This allows dematerialized inverses to be rematerialized + // we shouldn't be notifying here though, figure out where + // a notification was missed elsewhere. + if (!silenceNotifications) { + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } + } + } else { + relationship.remoteMembers.delete(value); + relationship.localMembers.delete(value); + } +} + +// 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 + console.log(`Graph: ignoring relationship change for removed identifier ${String(identifier)} ${key}`); + } + return; + } + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`Graph: notifying relationship change for ${String(identifier)} ${key}`); + } + + graph.store.notifyChange(identifier, 'relationships', key); +} + +export function assertRelationshipData( + store: Store, + identifier: StableRecordIdentifier, + data: ResourceIdentifierObject, + meta: UpgradedMeta +) { + assert( + `A ${identifier.type} record was pushed into the store with the value of ${meta.key} being '${JSON.stringify( + data + )}', but ${ + meta.key + } is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, + !Array.isArray(data) + ); + assert( + `Encountered a relationship identifier without a type for the ${meta.kind} relationship '${meta.key}' on <${ + identifier.type + }:${String(identifier.id)}>, expected an identifier with type '${meta.type}' but found\n\n'${JSON.stringify( + data, + null, + 2 + )}'\n\nPlease check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, + data === null || ('type' in data && typeof data.type === 'string' && data.type.length) + ); + assert( + `Encountered a relationship identifier without an id for the ${meta.kind} relationship '${meta.key}' on <${ + identifier.type + }:${String(identifier.id)}>, expected an identifier but found\n\n'${JSON.stringify( + data, + null, + 2 + )}'\n\nPlease check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, + data === null || !!coerceId(data.id) + ); + if (data?.type === meta.type) { + assert( + `Missing Schema: Encountered a relationship identifier { type: '${data.type}', id: '${String( + data.id + )}' } for the '${identifier.type}.${meta.key}' ${meta.kind} relationship on <${identifier.type}:${String( + identifier.id + )}>, but no schema exists for that type.`, + store.schema.hasResource(data) + ); + } else { + assert( + `Missing Schema: Encountered a relationship identifier with type '${data.type}' for the ${ + meta.kind + } relationship '${meta.key}' on <${identifier.type}:${String( + identifier.id + )}>, Expected an identifier with type '${meta.type}'. No schema was found for '${data.type}'.`, + data === null || !data.type || store.schema.hasResource(data) + ); + } +} diff --git a/packages/graph/src/-private/coerce-id.ts b/packages/graph/src/-private/coerce-id.ts index ebe779e014d..5d4337a6484 100644 --- a/packages/graph/src/-private/coerce-id.ts +++ b/packages/graph/src/-private/coerce-id.ts @@ -1,4 +1,7 @@ -import { DEBUG } from '@ember-data/env'; +import { deprecate } from '@ember/debug'; + +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 // that developers may provide IDs as numbers (e.g., `store.findRecord('person', 1)`), @@ -9,16 +12,39 @@ import { DEBUG } from '@ember-data/env'; type Coercable = string | number | boolean | null | undefined | symbol; export function coerceId(id: Coercable): string | null { - if (id === null || id === undefined || id === '') { - return null; - } - if (typeof id === 'string') { - return id; - } - if (typeof id === 'symbol') { - return id.toString(); + if (DEPRECATE_NON_STRICT_ID) { + let normalized: string | null; + if (id === null || id === undefined || id === '') { + normalized = null; + } else { + normalized = String(id); + } + + deprecate( + `The resource id '<${typeof id}> ${String( + id + )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, + { + id: 'ember-data:deprecate-non-strict-id', + until: '6.0', + for: 'ember-data', + since: { + available: '5.3', + enabled: '5.3', + }, + } + ); + + return normalized; } - return '' + id; + + assert( + `Resource IDs must be a non-empty string or null. Received '${String(id)}'.`, + id === null || (typeof id === 'string' && id.length > 0) + ); + + return id; } export function ensureStringId(id: Coercable): string { @@ -26,14 +52,10 @@ export function ensureStringId(id: Coercable): string { if (typeof id === 'string') { normalized = id.length > 0 ? id : null; } else if (typeof id === 'number' && !isNaN(id)) { - normalized = '' + id; + normalized = String(id); } - if (DEBUG) { - if (normalized === null) { - throw new Error(`Expected id to be a string or number, received ${String(id)}`); - } - } + assert(`Expected id to be a string or number, received ${String(id)}`, normalized !== null); - return normalized!; + return normalized; } diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.js b/packages/graph/src/-private/debug/assert-polymorphic-type.js deleted file mode 100644 index 7a64937b4c4..00000000000 --- a/packages/graph/src/-private/debug/assert-polymorphic-type.js +++ /dev/null @@ -1,247 +0,0 @@ -import { assert } from '@ember/debug'; -import { DEBUG } from '@ember-data/env'; - -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; - -/* - Assert that `addedRecord` has a valid type so it can be added to the - relationship of the `record`. - - The assert basically checks if the `addedRecord` can be added to the - relationship (specified via `relationshipMeta`) of the `record`. - - This utility should only be used internally, as both record parameters must - be stable record identifiers and the `relationshipMeta` needs to be the meta - information about the relationship, retrieved via - `record.relationshipFor(key)`. -*/ -let assertPolymorphicType; -let assertInheritedSchema; - -if (DEBUG) { - let checkPolymorphic = function checkPolymorphic(modelClass, addedModelClass) { - if (modelClass.__isMixin) { - return ( - modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || - // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` - modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) - ); - } - return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); - }; - - function validateSchema(definition, meta) { - const errors = new Map(); - - if (definition.inverseKey !== meta.name) { - errors.set('name', ` <---- should be '${definition.inverseKey}'`); - } - if (definition.inverseType !== meta.type) { - errors.set('type', ` <---- should be '${definition.inverseType}'`); - } - if (definition.inverseKind !== meta.kind) { - errors.set('type', ` <---- should be '${definition.inverseKind}'`); - } - if (definition.inverseIsAsync !== meta.options.async) { - errors.set('async', ` <---- should be ${definition.inverseIsAsync}`); - } - if (definition.inverseIsPolymorphic && definition.inverseIsPolymorphic !== meta.options.polymorphic) { - errors.set('polymorphic', ` <---- should be ${definition.inverseIsPolymorphic}`); - } - if (definition.key !== meta.options.inverse) { - errors.set('inverse', ` <---- should be '${definition.key}'`); - } - if (definition.type !== meta.options.as) { - errors.set('as', ` <---- should be '${definition.type}'`); - } - - return errors; - } - - function expectedSchema(definition) { - return printSchema({ - name: definition.inverseKey, - type: definition.inverseType, - kind: definition.inverseKind, - options: { - as: definition.type, - async: definition.inverseIsAsync, - polymorphic: definition.inverseIsPolymorphic || false, - inverse: definition.key - } - }); - } - - function printSchema(config, errors) { - return ` - -\`\`\` -{ - ${config.name}: { - name: '${config.name}',${errors?.get('name') || ''} - type: '${config.type}',${errors?.get('type') || ''} - kind: '${config.kind}',${errors?.get('kind') || ''} - options: { - as: '${config.options.as}',${errors?.get('as') || ''} - async: ${config.options.async},${errors?.get('async') || ''} - polymorphic: ${config.options.polymorphic},${errors?.get('polymorphic') || ''} - inverse: '${config.options.inverse}'${errors?.get('inverse') || ''} - } - } -} -\`\`\` - -` - } - - function metaFrom(definition) { - return { - name: definition.key, - type: definition.type, - kind: definition.kind, - options: { - async: definition.isAsync, - polymorphic: definition.isPolymorphic, - inverse: definition.inverseKey - } - }; - } - function inverseMetaFrom(definition) { - return { - name: definition.inverseKey, - type: definition.inverseType, - kind: definition.inverseKind, - options: { - as: definition.isPolymorphic ? definition.type : undefined, - async: definition.inverseIsAsync, - polymorphic: definition.inverseIsPolymorphic, - inverse: definition.key - } - }; - } - function inverseDefinition(definition) { - return { - key: definition.inverseKey, - type: definition.inverseType, - kind: definition.inverseKind, - isAsync: definition.inverseIsAsync, - isPolymorphic: true, - inverseKey: definition.key, - inverseType: definition.type, - inverseKind: definition.kind, - inverseIsAsync: definition.isAsync, - inverseIsPolymorphic: definition.isPolymorphic - }; - } - function definitionWithPolymorphic(definition) { - return Object.assign({}, definition, { inverseIsPolymorphic: true }); - } - - assertInheritedSchema = function assertInheritedSchema(definition, type) { - let meta1 = metaFrom(definition); - let meta2 = inverseMetaFrom(definition); - let errors1 = validateSchema(inverseDefinition(definition), meta1); - let errors2 = validateSchema(definitionWithPolymorphic(definition), meta2); - - if (errors2.size === 0 && errors1.size > 0) { - throw new Error(`The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${definition.inverseType}' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema(meta1, errors1)}`); - - } else if (errors1.size > 0) { - throw new Error(`The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${definition.inverseType}' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema(meta1, errors1)} and the relationships schema definition for '${definition.type}' should include:${printSchema(meta2, errors2)}`); - - } else if (errors2.size > 0) { - throw new Error(`The schema for the relationship '${type}.${definition.key}' satisfies '${definition.inverseType}' but cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${definition.type}.${definition.inverseKey}' because that relationship is not polymorphic.\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${definition.type}' should include:${printSchema(meta2, errors2)}`); - - } - } - - assertPolymorphicType = function assertPolymorphicType(parentIdentifier, parentDefinition, addedIdentifier, store) { - let asserted = false; - - if (parentDefinition.inverseIsImplicit) { - return; - } - if (parentDefinition.isPolymorphic) { - let meta = store.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; - 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) - ); - let 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( - `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) - ); - let 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.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; - - if (!DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { - if (meta?.options.as === parentDefinition.type) { - // inverse is likely polymorphic but missing the polymorphic flag - let meta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: parentDefinition.inverseType })[ - parentDefinition.key - ]; - let 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}'\`.`); - } - } else if (meta?.options?.as?.length > 0) { - asserted = true; - if (meta?.options.as === parentDefinition.type) { - // inverse is likely polymorphic but missing the polymorphic flag - let meta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: parentDefinition.inverseType })[ - parentDefinition.key - ]; - let 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) { - store = store._store ? store._store : store; // allow usage with storeWrapper - let addedModelName = addedIdentifier.type; - let parentModelName = parentIdentifier.type; - let key = parentDefinition.key; - let relationshipModelName = parentDefinition.type; - let relationshipClass = store.modelFor(relationshipModelName); - let addedClass = store.modelFor(addedModelName); - - let 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.`; - let isPolymorphic = checkPolymorphic(relationshipClass, addedClass); - - assert(assertionMessage, isPolymorphic); - } - } - }; -} - -export { assertPolymorphicType, assertInheritedSchema }; diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.ts b/packages/graph/src/-private/debug/assert-polymorphic-type.ts new file mode 100644 index 00000000000..ee164ad6bb8 --- /dev/null +++ b/packages/graph/src/-private/debug/assert-polymorphic-type.ts @@ -0,0 +1,394 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +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`. + + The assert basically checks if the `addedRecord` can be added to the + relationship (specified via `relationshipMeta`) of the `record`. + + This utility should only be used internally, as both record parameters must + be stable record identifiers and the `relationshipMeta` needs to be the meta + information about the relationship, retrieved via + `record.relationshipFor(key)`. +*/ +let assertPolymorphicType: ( + parentIdentifier: StableRecordIdentifier, + parentDefinition: UpgradedMeta, + addedIdentifier: StableRecordIdentifier, + store: CacheCapabilitiesManager +) => void; +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(); + + if (definition.inverseKey !== meta.name) { + errors.set('name', ` <---- should be '${definition.inverseKey}'`); + } + if (definition.inverseType !== meta.type) { + errors.set('type', ` <---- should be '${definition.inverseType}'`); + } + if (definition.inverseKind !== meta.kind) { + errors.set('type', ` <---- should be '${definition.inverseKind}'`); + } + if (definition.inverseIsAsync !== meta.options.async) { + errors.set('async', ` <---- should be ${definition.inverseIsAsync}`); + } + if (definition.inverseIsPolymorphic && definition.inverseIsPolymorphic !== meta.options.polymorphic) { + errors.set('polymorphic', ` <---- should be ${definition.inverseIsPolymorphic}`); + } + if (definition.key !== meta.options.inverse) { + errors.set('inverse', ` <---- should be '${definition.key}'`); + } + if (definition.type !== meta.options.as) { + errors.set('as', ` <---- should be '${definition.type}'`); + } + + return errors; + } + + type PrintConfig = { + name: string; + type: string; + kind: string; + options: { + as?: string; + async?: boolean; + polymorphic?: boolean; + inverse?: string | null; + }; + }; + type RelationshipSchemaError = 'name' | 'type' | 'kind' | 'as' | 'async' | 'polymorphic' | 'inverse'; + + function expectedSchema(definition: UpgradedMeta) { + return printSchema({ + name: definition.inverseKey, + type: definition.inverseType, + kind: definition.inverseKind, + options: { + as: definition.type, + async: definition.inverseIsAsync, + polymorphic: definition.inverseIsPolymorphic || false, + inverse: definition.key, + }, + }); + } + + function printSchema(config: PrintConfig, errors?: Map) { + return ` + +\`\`\` +{ + ${config.name}: { + name: '${config.name}',${errors?.get('name') || ''} + type: '${config.type}',${errors?.get('type') || ''} + kind: '${config.kind}',${errors?.get('kind') || ''} + options: { + as: '${config.options.as}',${errors?.get('as') || ''} + async: ${config.options.async},${errors?.get('async') || ''} + polymorphic: ${config.options.polymorphic},${errors?.get('polymorphic') || ''} + inverse: '${config.options.inverse}'${errors?.get('inverse') || ''} + } + } +} +\`\`\` + +`; + } + + function metaFrom(definition: UpgradedMeta) { + return { + name: definition.key, + type: definition.type, + kind: definition.kind, + options: { + async: definition.isAsync, + polymorphic: definition.isPolymorphic, + inverse: definition.inverseKey, + }, + }; + } + function inverseMetaFrom(definition: UpgradedMeta) { + return { + name: definition.inverseKey, + type: definition.inverseType, + kind: definition.inverseKind, + options: { + as: definition.isPolymorphic ? definition.type : undefined, + async: definition.inverseIsAsync, + polymorphic: definition.inverseIsPolymorphic, + inverse: definition.key, + }, + }; + } + function inverseDefinition(definition: UpgradedMeta): UpgradedMeta { + return { + key: definition.inverseKey, + type: definition.inverseType, + kind: definition.inverseKind, + isAsync: definition.inverseIsAsync, + isPolymorphic: true, + isCollection: definition.inverseIsCollection, + isImplicit: definition.inverseIsImplicit, + inverseKey: definition.key, + inverseType: definition.type, + inverseKind: definition.kind, + inverseIsAsync: definition.isAsync, + inverseIsPolymorphic: definition.isPolymorphic, + inverseIsImplicit: definition.isImplicit, + inverseIsCollection: definition.isCollection, + resetOnRemoteUpdate: definition.resetOnRemoteUpdate, + }; + } + function definitionWithPolymorphic(definition: UpgradedMeta) { + return Object.assign({}, definition, { inverseIsPolymorphic: true }); + } + + assertInheritedSchema = function assertInheritedSchema(definition: UpgradedMeta, type: string) { + const meta1 = metaFrom(definition); + const meta2 = inverseMetaFrom(definition); + const errors1 = validateSchema(inverseDefinition(definition), meta1); + const errors2 = validateSchema(definitionWithPolymorphic(definition), meta2); + + if (errors2.size === 0 && errors1.size > 0) { + throw new Error( + `The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${ + definition.inverseType + }' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${ + definition.type + }.${ + definition.inverseKey + }'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema( + meta1, + errors1 + )}` + ); + } else if (errors1.size > 0) { + throw new Error( + `The schema for the relationship '${type}.${definition.key}' is not configured to satisfy '${ + definition.inverseType + }' and thus cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${ + definition.type + }.${ + definition.inverseKey + }'\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${type}' should include:${printSchema( + meta1, + errors1 + )} and the relationships schema definition for '${definition.type}' should include:${printSchema( + meta2, + errors2 + )}` + ); + } else if (errors2.size > 0) { + throw new Error( + `The schema for the relationship '${type}.${definition.key}' satisfies '${ + definition.inverseType + }' but cannot utilize the '${definition.inverseType}.${definition.key}' relationship to connect with '${ + definition.type + }.${ + definition.inverseKey + }' because that relationship is not polymorphic.\n\nIf using this relationship in a polymorphic manner is desired, the relationships schema definition for '${ + definition.type + }' should include:${printSchema(meta2, errors2)}` + ); + } + }; + + assertPolymorphicType = function assertPolymorphicType( + parentIdentifier: StableRecordIdentifier, + parentDefinition: UpgradedMeta, + addedIdentifier: StableRecordIdentifier, + store: CacheCapabilitiesManager + ) { + if (parentDefinition.inverseIsImplicit) { + return; + } + let asserted = false; + + if (parentDefinition.isPolymorphic) { + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); + assert( + `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 + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); + assert( + `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 === 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( + `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); + } + } + }; +} + +export { assertPolymorphicType, assertInheritedSchema }; diff --git a/packages/graph/src/-private/edges/collection.ts b/packages/graph/src/-private/edges/collection.ts new file mode 100644 index 00000000000..ed61a0c6a7b --- /dev/null +++ b/packages/graph/src/-private/edges/collection.ts @@ -0,0 +1,70 @@ +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Links, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +import { computeLocalState } from '../-diff'; +import type { UpgradedMeta } from '../-edge-definition'; +import type { RelationshipState } from '../-state'; +import { createState } from '../-state'; + +export interface CollectionEdge { + definition: UpgradedMeta; + identifier: StableRecordIdentifier; + state: RelationshipState; + + remoteMembers: Set; + remoteState: StableRecordIdentifier[]; + + additions: Set | null; + removals: Set | null; + + meta: Meta | null; + links: Links | PaginationLinks | null; + + localState: StableRecordIdentifier[] | null; + isDirty: boolean; + transactionRef: number; + + _diff?: { + add: Set; + del: Set; + }; +} + +export function createCollectionEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): CollectionEdge { + return { + definition, + identifier, + state: createState(), + remoteMembers: new Set(), + remoteState: [], + additions: null, + removals: null, + + meta: null, + links: null, + + localState: null, + isDirty: true, + transactionRef: 0, + _diff: undefined, + }; +} + +export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship { + const payload: CollectionRelationship = {}; + + if (source.state.hasReceivedData) { + payload.data = computeLocalState(source); + } + + if (source.links) { + payload.links = source.links; + } + + if (source.meta) { + payload.meta = source.meta; + } + + return payload; +} diff --git a/packages/graph/src/-private/edges/implicit.ts b/packages/graph/src/-private/edges/implicit.ts new file mode 100644 index 00000000000..89d7074eb5e --- /dev/null +++ b/packages/graph/src/-private/edges/implicit.ts @@ -0,0 +1,21 @@ +import type { StableRecordIdentifier } from '@warp-drive/core-types'; + +import type { UpgradedMeta } from '../-edge-definition'; + +export type ImplicitMeta = UpgradedMeta & { kind: 'implicit'; isImplicit: true }; + +export interface ImplicitEdge { + definition: ImplicitMeta; + identifier: StableRecordIdentifier; + localMembers: Set; + remoteMembers: Set; +} + +export function createImplicitEdge(definition: ImplicitMeta, identifier: StableRecordIdentifier): ImplicitEdge { + return { + definition, + identifier, + localMembers: new Set(), + remoteMembers: new Set(), + }; +} diff --git a/packages/graph/src/-private/edges/resource.ts b/packages/graph/src/-private/edges/resource.ts new file mode 100644 index 00000000000..3d5c615335a --- /dev/null +++ b/packages/graph/src/-private/edges/resource.ts @@ -0,0 +1,61 @@ +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Links, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { UpgradedMeta } from '../-edge-definition'; +import type { RelationshipState } from '../-state'; +import { createState } from '../-state'; + +/* + * @module @ember-data/graph + * + * Stores the data for one side of a "single" resource relationship. + * + * @class ResourceEdge + * @internal + */ +export interface ResourceEdge { + definition: UpgradedMeta; + identifier: StableRecordIdentifier; + state: RelationshipState; + localState: StableRecordIdentifier | null; + remoteState: StableRecordIdentifier | null; + meta: Meta | null; + links: Links | PaginationLinks | null; + transactionRef: number; +} + +export function createResourceEdge(definition: UpgradedMeta, identifier: StableRecordIdentifier): ResourceEdge { + return { + definition, + identifier, + state: createState(), + transactionRef: 0, + localState: null, + remoteState: null, + meta: null, + links: null, + }; +} + +export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship { + let data: StableRecordIdentifier | null | undefined; + const payload: ResourceRelationship = {}; + if (source.localState) { + data = source.localState; + } + if (source.localState === null && source.state.hasReceivedData) { + data = null; + } + if (source.links) { + payload.links = source.links; + } + if (data !== undefined) { + payload.data = data; + } + if (source.meta) { + payload.meta = source.meta; + } + + return payload; +} diff --git a/packages/graph/src/-private/graph.ts b/packages/graph/src/-private/graph.ts new file mode 100644 index 00000000000..b2a5b5b87f2 --- /dev/null +++ b/packages/graph/src/-private/graph.ts @@ -0,0 +1,818 @@ +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import { LOG_GRAPH } from '@warp-drive/build-config/debugging'; +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 { getOrSetGlobal, peekTransient, setTransient } from '@warp-drive/core-types/-private'; +import type { RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { + DeleteRecordOperation, + LocalRelationshipOperation, + RemoteRelationshipOperation, + UnknownOperation, +} from '@warp-drive/core-types/graph'; + +import { rollbackRelationship } from './-diff'; +import type { EdgeCache, UpgradedMeta } from './-edge-definition'; +import { isLHS, upgradeDefinition } from './-edge-definition'; +import { + assertValidRelationshipPayload, + forAllRelatedIdentifiers, + getStore, + isBelongsTo, + isHasMany, + isImplicit, + isNew, + notifyChange, + removeIdentifierCompletelyFromRelationship, +} from './-utils'; +import { type CollectionEdge, createCollectionEdge, legacyGetCollectionRelationshipData } from './edges/collection'; +import type { ImplicitEdge, ImplicitMeta } from './edges/implicit'; +import { createImplicitEdge } from './edges/implicit'; +import { createResourceEdge, legacyGetResourceRelationshipData, type ResourceEdge } from './edges/resource'; +import addToRelatedRecords from './operations/add-to-related-records'; +import { mergeIdentifier } from './operations/merge-identifier'; +import removeFromRelatedRecords from './operations/remove-from-related-records'; +import replaceRelatedRecord from './operations/replace-related-record'; +import replaceRelatedRecords from './operations/replace-related-records'; +import updateRelationshipOperation from './operations/update-relationship'; + +export type GraphEdge = ImplicitEdge | CollectionEdge | ResourceEdge; + +export const Graphs = getOrSetGlobal('Graphs', new Map()); + +type PendingOps = { + belongsTo?: Map>; + hasMany?: Map>; + deletions: DeleteRecordOperation[]; +}; + +/* + * Graph acts as the cache for relationship data. It allows for + * us to ask about and update relationships for a given Identifier + * without requiring other objects for that Identifier to be + * instantiated (such as `RecordData` or a `Record`) + * + * This also allows for us to make more substantive changes to relationships + * with increasingly minor alterations to other portions of the internals + * over time. + * + * The graph is made up of nodes and edges. Each unique identifier gets + * its own node, which is a dictionary with a list of that node's edges + * (or connections) to other nodes. In `Model` terms, a node represents a + * record instance, with each key (an edge) in the dictionary correlating + * to either a `hasMany` or `belongsTo` field on that record instance. + * + * The value for each key, or `edge` is the identifier(s) the node relates + * to in the graph from that key. + */ +export class Graph { + declare _definitionCache: EdgeCache; + declare _metaCache: Record>; + declare _potentialPolymorphicTypes: Record>; + declare identifiers: Map>; + declare store: CacheCapabilitiesManager; + declare isDestroyed: boolean; + declare _willSyncRemote: boolean; + declare _willSyncLocal: boolean; + declare silenceNotifications: boolean; + declare _pushedUpdates: PendingOps; + declare _updatedRelationships: Set; + declare _transaction: number | null; + declare _removing: StableRecordIdentifier | null; + + constructor(store: CacheCapabilitiesManager) { + this._definitionCache = Object.create(null) as EdgeCache; + this._metaCache = Object.create(null) as Record>; + this._potentialPolymorphicTypes = Object.create(null) as Record>; + this.identifiers = new Map(); + this.store = store; + this.isDestroyed = false; + this._willSyncRemote = false; + this._willSyncLocal = false; + this._pushedUpdates = { + belongsTo: undefined, + hasMany: undefined, + deletions: [], + }; + this._updatedRelationships = new Set(); + this._transaction = null; + this._removing = null; + this.silenceNotifications = false; + } + + has(identifier: StableRecordIdentifier, propertyName: string): boolean { + const relationships = this.identifiers.get(identifier); + if (!relationships) { + return false; + } + return relationships[propertyName] !== undefined; + } + + getDefinition(identifier: StableRecordIdentifier, propertyName: string): UpgradedMeta { + let defs = this._metaCache[identifier.type]; + let meta: UpgradedMeta | null | undefined = defs?.[propertyName]; + if (!meta) { + const info = /*#__NOINLINE__*/ upgradeDefinition(this, identifier, propertyName); + assert(`Could not determine relationship information for ${identifier.type}.${propertyName}`, info !== null); + + // if (info.rhs_definition?.kind === 'implicit') { + // we should possibly also do this + // but it would result in being extremely permissive for other relationships by accident + // this.registerPolymorphicType(info.rhs_baseModelName, identifier.type); + // } + + meta = /*#__NOINLINE__*/ isLHS(info, identifier.type, propertyName) ? info.lhs_definition : info.rhs_definition!; + defs = this._metaCache[identifier.type] = defs || {}; + defs[propertyName] = meta; + } + return meta; + } + + get(identifier: StableRecordIdentifier, propertyName: string): GraphEdge { + assert(`expected propertyName`, propertyName); + let relationships = this.identifiers.get(identifier); + if (!relationships) { + relationships = Object.create(null) as Record; + this.identifiers.set(identifier, relationships); + } + + let relationship = relationships[propertyName]; + if (!relationship) { + const meta = this.getDefinition(identifier, propertyName); + + if (meta.kind === 'belongsTo') { + relationship = relationships[propertyName] = createResourceEdge(meta, identifier); + } else if (meta.kind === 'hasMany') { + relationship = relationships[propertyName] = createCollectionEdge(meta, identifier); + } else { + assert(`Expected kind to be implicit`, meta.kind === 'implicit' && meta.isImplicit === true); + relationship = relationships[propertyName] = createImplicitEdge(meta as ImplicitMeta, identifier); + } + } + + return relationship; + } + + getData(identifier: StableRecordIdentifier, propertyName: string): ResourceRelationship | CollectionRelationship { + const relationship = this.get(identifier, propertyName); + + assert(`Cannot getData() on an implicit relationship`, !isImplicit(relationship)); + + if (isBelongsTo(relationship)) { + return legacyGetResourceRelationshipData(relationship); + } + + return legacyGetCollectionRelationshipData(relationship); + } + + /* + * Allows for the graph to dynamically discover polymorphic connections + * without needing to walk prototype chains. + * + * Used by edges when an added `type` does not match the expected `type` + * for that edge. + * + * Currently we assert before calling this. For a public API we will want + * to call out to the schema manager to ask if we should consider these + * types as equivalent for a given relationship. + */ + registerPolymorphicType(type1: string, type2: string): void { + const typeCache = this._potentialPolymorphicTypes; + let t1 = typeCache[type1]; + if (!t1) { + t1 = typeCache[type1] = Object.create(null) as Record; + } + t1[type2] = true; + + let t2 = typeCache[type2]; + if (!t2) { + t2 = typeCache[type2] = Object.create(null) as Record; + } + t2[type1] = true; + } + + /* + TODO move this comment somewhere else + implicit relationships are relationships which have not been declared but the inverse side exists on + another record somewhere + + For example if there was: + + ```app/models/comment.js + import Model, { attr } from '@ember-data/model'; + + export default class Comment extends Model { + @attr text; + } + ``` + + and there is also: + + ```app/models/post.js + import Model, { attr, hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @attr title; + @hasMany('comment', { async: true, inverse: null }) comments; + } + ``` + + Then we would have a implicit 'post' relationship for the comment record in order + to be do things like remove the comment from the post if the comment were to be deleted. + */ + + isReleasable(identifier: StableRecordIdentifier): boolean { + const relationships = this.identifiers.get(identifier); + if (!relationships) { + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: RELEASABLE ${String(identifier)}`); + } + return true; + } + const keys = Object.keys(relationships); + for (let i = 0; i < keys.length; i++) { + const relationship: GraphEdge = relationships[keys[i]]; + // account for previously unloaded relationships + // typically from a prior deletion of a record that pointed to this one implicitly + if (relationship === undefined) { + continue; + } + assert(`Expected a relationship`, relationship); + if (relationship.definition.inverseIsAsync && !isNew(identifier)) { + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: <> RELEASABLE ${String(identifier)}`); + } + return false; + } + } + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: RELEASABLE ${String(identifier)}`); + } + return true; + } + + unload(identifier: StableRecordIdentifier, silenceNotifications?: boolean) { + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: unload ${String(identifier)}`); + } + const relationships = this.identifiers.get(identifier); + + if (relationships) { + // cleans up the graph but retains some nodes + // to allow for rematerialization + Object.keys(relationships).forEach((key) => { + const rel = relationships[key]; + if (!rel) { + return; + } + /*#__NOINLINE__*/ destroyRelationship(this, rel, silenceNotifications); + if (/*#__NOINLINE__*/ isImplicit(rel)) { + // @ts-expect-error + relationships[key] = undefined; + } + }); + } + } + + _isDirty(identifier: StableRecordIdentifier, field: string): boolean { + const relationships = this.identifiers.get(identifier); + if (!relationships) { + return false; + } + const relationship = relationships[field]; + if (!relationship) { + return false; + } + if (isBelongsTo(relationship)) { + return relationship.localState !== relationship.remoteState; + } else if (isHasMany(relationship)) { + const hasAdditions = relationship.additions !== null && relationship.additions.size > 0; + const hasRemovals = relationship.removals !== null && relationship.removals.size > 0; + return hasAdditions || hasRemovals || isReordered(relationship); + } + return false; + } + + getChanged(identifier: StableRecordIdentifier): Map { + const relationships = this.identifiers.get(identifier); + const changed = new Map(); + + if (!relationships) { + return changed; + } + + const keys = Object.keys(relationships); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + const relationship = relationships[field]; + if (!relationship) { + continue; + } + if (isBelongsTo(relationship)) { + if (relationship.localState !== relationship.remoteState) { + changed.set(field, { + kind: 'resource', + remoteState: relationship.remoteState, + localState: relationship.localState, + }); + } + } else if (isHasMany(relationship)) { + const hasAdditions = relationship.additions !== null && relationship.additions.size > 0; + const hasRemovals = relationship.removals !== null && relationship.removals.size > 0; + const reordered = isReordered(relationship); + + if (hasAdditions || hasRemovals || reordered) { + changed.set(field, { + kind: 'collection', + additions: new Set(relationship.additions), + removals: new Set(relationship.removals), + remoteState: relationship.remoteState, + localState: legacyGetCollectionRelationshipData(relationship).data || [], + reordered, + }); + } + } + } + + return changed; + } + + hasChanged(identifier: StableRecordIdentifier): boolean { + const relationships = this.identifiers.get(identifier); + if (!relationships) { + return false; + } + const keys = Object.keys(relationships); + for (let i = 0; i < keys.length; i++) { + if (this._isDirty(identifier, keys[i])) { + return true; + } + } + return false; + } + + rollback(identifier: StableRecordIdentifier): string[] { + const relationships = this.identifiers.get(identifier); + const changed: string[] = []; + if (!relationships) { + return changed; + } + const keys = Object.keys(relationships); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + const relationship = relationships[field]; + if (!relationship) { + continue; + } + + if (this._isDirty(identifier, field)) { + rollbackRelationship(this, identifier, field, relationship as CollectionEdge | ResourceEdge); + changed.push(field); + } + } + + return changed; + } + + remove(identifier: StableRecordIdentifier) { + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: remove ${String(identifier)}`); + } + assert(`Cannot remove ${String(identifier)} while still removing ${String(this._removing)}`, !this._removing); + this._removing = identifier; + this.unload(identifier); + this.identifiers.delete(identifier); + this._removing = null; + } + + /* + * Remote state changes + */ + push(op: RemoteRelationshipOperation) { + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: push ${String(op.record)}`, op); + } + if (op.op === 'deleteRecord') { + this._pushedUpdates.deletions.push(op); + } else { + const definition = this.getDefinition(op.record, op.field); + assert(`Cannot push a remote update for an implicit relationship`, definition.kind !== 'implicit'); + addPending(this._pushedUpdates, definition, op); + } + if (!this._willSyncRemote) { + this._willSyncRemote = true; + getStore(this.store)._schedule('coalesce', () => this._flushRemoteQueue()); + } + } + + /* + * Local state changes + */ + update(op: RemoteRelationshipOperation | MergeOperation, isRemote: true): void; + update(op: LocalRelationshipOperation, isRemote?: false): void; + update( + op: MergeOperation | LocalRelationshipOperation | RemoteRelationshipOperation | UnknownOperation, + isRemote = false + ): void { + assert( + `Cannot update an implicit relationship`, + op.op === 'deleteRecord' || op.op === 'mergeIdentifiers' || !isImplicit(this.get(op.record, op.field)) + ); + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`graph: update (${isRemote ? 'remote' : 'local'}) ${String(op.record)}`, op); + } + + switch (op.op) { + case 'mergeIdentifiers': { + const relationships = this.identifiers.get(op.record); + if (relationships) { + /*#__NOINLINE__*/ mergeIdentifier(this, op, relationships); + } + break; + } + case 'updateRelationship': + assert(`Can only perform the operation updateRelationship on remote state`, isRemote); + if (DEBUG) { + // in debug, assert payload validity eagerly + // TODO add deprecations/assertion here for duplicates + assertValidRelationshipPayload(this, op); + } + /*#__NOINLINE__*/ updateRelationshipOperation(this, op); + break; + case 'deleteRecord': { + assert(`Can only perform the operation deleteRelationship on remote state`, isRemote); + const identifier = op.record; + const relationships = this.identifiers.get(identifier); + + if (relationships) { + Object.keys(relationships).forEach((key) => { + const rel = relationships[key]; + if (!rel) { + return; + } + // works together with the has check + // @ts-expect-error + relationships[key] = undefined; + /*#__NOINLINE__*/ removeCompletelyFromInverse(this, rel); + }); + this.identifiers.delete(identifier); + } + break; + } + case 'replaceRelatedRecord': + /*#__NOINLINE__*/ replaceRelatedRecord(this, op, isRemote); + break; + case 'addToRelatedRecords': + // we will lift this restriction once the cache is allowed to make remote updates directly + assert(`Can only perform the operation addToRelatedRecords on local state`, !isRemote); + /*#__NOINLINE__*/ addToRelatedRecords(this, op, isRemote); + break; + case 'removeFromRelatedRecords': + // we will lift this restriction once the cache is allowed to make remote updates directly + assert(`Can only perform the operation removeFromRelatedRecords on local state`, !isRemote); + /*#__NOINLINE__*/ removeFromRelatedRecords(this, op, isRemote); + break; + case 'replaceRelatedRecords': + /*#__NOINLINE__*/ replaceRelatedRecords(this, op, isRemote); + break; + default: + assert(`No local relationship update operation exists for '${op.op}'`); + } + } + + _scheduleLocalSync(relationship: CollectionEdge) { + this._updatedRelationships.add(relationship); + if (!this._willSyncLocal) { + this._willSyncLocal = true; + getStore(this.store)._schedule('sync', () => this._flushLocalQueue()); + } + } + + _flushRemoteQueue() { + if (!this._willSyncRemote) { + return; + } + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.groupCollapsed(`Graph: Initialized Transaction`); + } + let transactionRef = peekTransient('transactionRef') ?? 0; + this._transaction = ++transactionRef; + setTransient('transactionRef', transactionRef); + this._willSyncRemote = false; + const updates = this._pushedUpdates; + const { deletions, hasMany, belongsTo } = updates; + updates.deletions = []; + updates.hasMany = undefined; + updates.belongsTo = undefined; + + for (let i = 0; i < deletions.length; i++) { + this.update(deletions[i], true); + } + + if (hasMany) { + flushPending(this, hasMany); + } + if (belongsTo) { + flushPending(this, belongsTo); + } + + this._transaction = null; + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`Graph: transaction finalized`); + // eslint-disable-next-line no-console + console.groupEnd(); + } + } + + _addToTransaction(relationship: CollectionEdge | ResourceEdge) { + assert(`expected a transaction`, this._transaction !== null); + if (LOG_GRAPH) { + // eslint-disable-next-line no-console + console.log(`Graph: ${String(relationship.identifier)} ${relationship.definition.key} added to transaction`); + } + relationship.transactionRef = this._transaction; + } + + _flushLocalQueue() { + if (!this._willSyncLocal) { + return; + } + + if (this.silenceNotifications) { + this.silenceNotifications = false; + this._updatedRelationships = new Set(); + return; + } + + this._willSyncLocal = false; + const updated = this._updatedRelationships; + this._updatedRelationships = new Set(); + updated.forEach((rel) => notifyChange(this, rel.identifier, rel.definition.key)); + } + + destroy() { + Graphs.delete(this.store); + + if (DEBUG) { + Graphs.delete(getStore(this.store) as unknown as CacheCapabilitiesManager); + if (Graphs.size) { + Graphs.forEach((_, key) => { + assert( + `Memory Leak Detected, likely the test or app instance previous to this was not torn down properly`, + !(key as unknown as { isDestroyed: boolean }).isDestroyed && + !(key as unknown as { isDestroying: boolean }).isDestroying + ); + }); + } + } + + this.identifiers.clear(); + this.store = null as unknown as CacheCapabilitiesManager; + this.isDestroyed = true; + } +} + +function flushPending(graph: Graph, ops: Map>) { + ops.forEach((type) => { + type.forEach((opList) => { + flushPendingList(graph, opList); + }); + }); +} +function flushPendingList(graph: Graph, opList: RemoteRelationshipOperation[]) { + for (let i = 0; i < opList.length; i++) { + graph.update(opList[i], true); + } +} + +// Handle dematerialization for relationship `rel`. In all cases, notify the +// relationship of the dematerialization: this is done so the relationship can +// notify its inverse which needs to update state +// +// If the inverse is sync, unloading this record is treated as a client-side +// delete, so we remove the inverse records from this relationship to +// disconnect the graph. Because it's not async, we don't need to keep around +// the identifier as an id-wrapper for references +function destroyRelationship(graph: Graph, rel: GraphEdge, silenceNotifications?: boolean) { + if (isImplicit(rel)) { + if (graph.isReleasable(rel.identifier)) { + /*#__NOINLINE__*/ removeCompletelyFromInverse(graph, rel); + } + return; + } + + const { identifier } = rel; + const { inverseKey } = rel.definition; + + if (!rel.definition.inverseIsImplicit) { + /*#__NOINLINE__*/ forAllRelatedIdentifiers(rel, (inverseIdentifer: StableRecordIdentifier) => + /*#__NOINLINE__*/ notifyInverseOfDematerialization( + graph, + inverseIdentifer, + inverseKey, + identifier, + silenceNotifications + ) + ); + } + + if (!rel.definition.inverseIsImplicit && !rel.definition.inverseIsAsync) { + rel.state.isStale = true; + /*#__NOINLINE__*/ clearRelationship(rel); + + // necessary to clear relationships in the ui from dematerialized records + // hasMany is managed by Model which calls `retreiveLatest` after + // dematerializing the resource-cache instance. + // but sync belongsTo requires this since they don't have a proxy to update. + // so we have to notify so it will "update" to null. + // we should discuss whether we still care about this, probably fine to just + // 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.identifier, rel.definition.key); + } + } +} + +function notifyInverseOfDematerialization( + graph: Graph, + inverseIdentifier: StableRecordIdentifier, + inverseKey: string, + identifier: StableRecordIdentifier, + silenceNotifications?: boolean +) { + if (!graph.has(inverseIdentifier, inverseKey)) { + return; + } + + const relationship = graph.get(inverseIdentifier, inverseKey); + assert(`expected no implicit`, !isImplicit(relationship)); + + // For remote members, it is possible that inverseRecordData has already been associated to + // to another record. For such cases, do not dematerialize the inverseRecordData + if (!isBelongsTo(relationship) || !relationship.localState || identifier === relationship.localState) { + /*#__NOINLINE__*/ removeDematerializedInverse(graph, relationship, identifier, silenceNotifications); + } +} + +function clearRelationship(relationship: CollectionEdge | ResourceEdge) { + if (isBelongsTo(relationship)) { + relationship.localState = null; + relationship.remoteState = null; + relationship.state.hasReceivedData = false; + relationship.state.isEmpty = true; + } else { + relationship.remoteMembers.clear(); + relationship.remoteState = []; + relationship.additions = null; + relationship.removals = null; + relationship.localState = null; + } +} + +function removeDematerializedInverse( + graph: Graph, + relationship: CollectionEdge | ResourceEdge, + inverseIdentifier: StableRecordIdentifier, + silenceNotifications?: boolean +) { + if (isBelongsTo(relationship)) { + const localInverse = relationship.localState; + if (!relationship.definition.isAsync || (localInverse && isNew(localInverse))) { + // unloading inverse of a sync relationship is treated as a client-side + // delete, so actually remove the models don't merely invalidate the cp + // cache. + // if the record being unloaded only exists on the client, we similarly + // treat it as a client side delete + if (relationship.localState === localInverse && localInverse !== null) { + relationship.localState = null; + } + + if (relationship.remoteState === localInverse && localInverse !== null) { + relationship.remoteState = null; + relationship.state.hasReceivedData = true; + relationship.state.isEmpty = true; + if (relationship.localState && !isNew(relationship.localState)) { + relationship.localState = null; + } + } + } else { + relationship.state.hasDematerializedInverse = true; + } + + if (!silenceNotifications) { + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } else { + if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) { + // unloading inverse of a sync relationship is treated as a client-side + // delete, so actually remove the models don't merely invalidate the cp + // cache. + // if the record being unloaded only exists on the client, we similarly + // treat it as a client side delete + /*#__NOINLINE__*/ removeIdentifierCompletelyFromRelationship(graph, relationship, inverseIdentifier); + } else { + relationship.state.hasDematerializedInverse = true; + } + + if (!silenceNotifications) { + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } +} + +function removeCompletelyFromInverse(graph: Graph, relationship: GraphEdge) { + const { identifier } = relationship; + const { inverseKey } = relationship.definition; + + forAllRelatedIdentifiers(relationship, (inverseIdentifier: StableRecordIdentifier) => { + if (graph.has(inverseIdentifier, inverseKey)) { + removeIdentifierCompletelyFromRelationship(graph, graph.get(inverseIdentifier, inverseKey), identifier); + } + }); + + if (isBelongsTo(relationship)) { + if (!relationship.definition.isAsync) { + clearRelationship(relationship); + } + + relationship.localState = null; + } else if (isHasMany(relationship)) { + if (!relationship.definition.isAsync) { + clearRelationship(relationship); + + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } else { + relationship.remoteMembers.clear(); + relationship.localMembers.clear(); + } +} + +function addPending( + cache: PendingOps, + definition: UpgradedMeta, + op: RemoteRelationshipOperation & { field: string } +): void { + const lc = (cache[definition.kind as 'hasMany' | 'belongsTo'] = + cache[definition.kind as 'hasMany' | 'belongsTo'] || new Map>()); + let lc2 = lc.get(definition.inverseType); + if (!lc2) { + lc2 = new Map(); + lc.set(definition.inverseType, lc2); + } + let arr = lc2.get(op.field); + if (!arr) { + arr = []; + lc2.set(op.field, arr); + } + arr.push(op); +} + +function isReordered(relationship: CollectionEdge): boolean { + // if we are dirty we are never re-ordered because accessing + // the state would flush away any reordering. + if (relationship.isDirty) { + return false; + } + + const { remoteState, localState, additions, removals } = relationship; + assert(`Expected localSate`, localState); + + for (let i = 0, j = 0; i < remoteState.length; i++) { + const member = remoteState[i]; + const localMember = localState[j]; + + if (member !== localMember) { + if (removals && removals.has(member)) { + // dont increment j because we want to skip this + continue; + } + if (additions && additions.has(localMember)) { + // increment j to skip this localMember + // decrement i to repeat this remoteMember + j++; + i--; + continue; + } + return true; + } + + // if we made it here, increment j + j++; + } + + return false; +} diff --git a/packages/graph/src/-private/graph/-edge-definition.ts b/packages/graph/src/-private/graph/-edge-definition.ts deleted file mode 100644 index 8ad2d56ba22..00000000000 --- a/packages/graph/src/-private/graph/-edge-definition.ts +++ /dev/null @@ -1,557 +0,0 @@ -import { assert } from '@ember/debug'; - -import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import type { RelationshipDefinition } from '@ember-data/model/-private/relationship-meta'; -import type Store from '@ember-data/store'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { Dict } from '@ember-data/types/q/utils'; - -import { assertInheritedSchema } from '../debug/assert-polymorphic-type'; -import { expandingGet, expandingSet, getStore } from './-utils'; -import type { Graph } from './graph'; - -export type EdgeCache = Dict>; - -/** - * - * Given RHS (Right Hand Side) - * - * ```ts - * class User extends Model { - * @hasMany('animal', { async: false, inverse: 'owner' }) pets; - * } - * ``` - * - * Given LHS (Left Hand Side) - * - * ```ts - * class Animal extends Model { - * @belongsTo('user', { async: false, inverse: 'pets' }) owner; - * } - * ``` - * - * The UpgradedMeta for the RHS would be: - * - * ```ts - * { - * kind: 'hasMany', - * key: 'pets', - * type: 'animal', - * isAsync: false, - * isImplicit: false, - * isCollection: true, - * isPolymorphic: false, - * inverseKind: 'belongsTo', - * inverseKey: 'owner', - * inverseType: 'user', - * inverseIsAsync: false, - * inverseIsImplicit: false, - * inverseIsCollection: false, - * inverseIsPolymorphic: false, - * } - * - * The UpgradeMeta for the LHS would be: - * - * ```ts - * { - * kind: 'belongsTo', - * key: 'owner', - * type: 'user', - * isAsync: false, - * isImplicit: false, - * isCollection: false, - * isPolymorphic: false, - * inverseKind: 'hasMany', - * inverseKey: 'pets', - * inverseType: 'animal', - * inverseIsAsync: false, - * inverseIsImplicit: false, - * inverseIsCollection: true, - * inverseIsPolymorphic: false, - * } - * ``` - * - * - * @class UpgradedMeta - * @internal - */ -export interface UpgradedMeta { - kind: 'hasMany' | 'belongsTo' | 'implicit'; - /** - * The field name on `this` record - * - * @internal - */ - key: string; - /** - * The `type` of the related record - * - * @internal - */ - type: string; - isAsync: boolean; - isImplicit: boolean; - isCollection: boolean; - isPolymorphic: boolean; - - inverseKind: 'hasMany' | 'belongsTo' | 'implicit'; - /** - * The field name on the opposing record - * @internal - */ - inverseKey: string; - /** - * The `type` of `this` record - * @internal - */ - inverseType: string; - inverseIsAsync: boolean; - inverseIsImplicit: boolean; - inverseIsCollection: boolean; - inverseIsPolymorphic: boolean; -} - -export interface EdgeDefinition { - lhs_key: string; - lhs_modelNames: string[]; - lhs_baseModelName: string; - lhs_relationshipName: string; - lhs_definition: UpgradedMeta; - lhs_isPolymorphic: boolean; - - rhs_key: string; - rhs_modelNames: string[]; - rhs_baseModelName: string; - rhs_relationshipName: string; - rhs_definition: UpgradedMeta | null; - rhs_isPolymorphic: boolean; - - hasInverse: boolean; - - /** - * Whether this relationship points back at the same type. - * - * If the relationship is polymorphic, this will be true if - * it points back at the same abstract type. - * - * @internal - */ - isSelfReferential: boolean; - - /** - * If this is a reflexive relationship, this is true - * if the relationship also points back at the same - * field. - * - * @internal - */ - isReflexive: boolean; -} - -const BOOL_LATER = null as unknown as boolean; -const STR_LATER = ''; -const IMPLICIT_KEY_RAND = Date.now(); - -function implicitKeyFor(type: string, key: string): string { - return `implicit-${type}:${key}${IMPLICIT_KEY_RAND}`; -} - -function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { - definition.inverseKind = inverseDefinition.kind; - definition.inverseKey = inverseDefinition.key; - definition.inverseType = inverseDefinition.type; - definition.inverseIsAsync = inverseDefinition.isAsync; - definition.inverseIsCollection = inverseDefinition.isCollection; - definition.inverseIsPolymorphic = inverseDefinition.isPolymorphic; - definition.inverseIsImplicit = inverseDefinition.isImplicit; -} - -function upgradeMeta(meta: RelationshipSchema): UpgradedMeta { - let niceMeta: UpgradedMeta = {} as UpgradedMeta; - let options = meta.options; - niceMeta.kind = meta.kind; - niceMeta.key = meta.name; - niceMeta.type = meta.type; - assert(`Expected relationship definition to specify async`, typeof options?.async === 'boolean'); - niceMeta.isAsync = options.async; - niceMeta.isImplicit = false; - niceMeta.isCollection = meta.kind === 'hasMany'; - niceMeta.isPolymorphic = options && !!options.polymorphic; - - 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; - - return niceMeta; -} - -function assertConfiguration(info: EdgeDefinition, type: string, key: string) { - if (DEBUG) { - let isSelfReferential = info.isSelfReferential; - - if (isSelfReferential) { - return true; - } - - let isRHS = - key === info.rhs_relationshipName && - (type === info.rhs_baseModelName || // base or non-polymorphic - // if the other side is polymorphic then we need to scan our modelNames - (info.lhs_isPolymorphic && info.rhs_modelNames.indexOf(type) !== -1)); // polymorphic - let isLHS = - key === info.lhs_relationshipName && - (type === info.lhs_baseModelName || // base or non-polymorphic - // if the other side is polymorphic then we need to scan our modelNames - (info.rhs_isPolymorphic && info.lhs_modelNames.indexOf(type) !== -1)); // polymorphic; - - if (!isRHS && !isLHS) { - /* - this occurs when we are likely polymorphic but not configured to be polymorphic - most often due to extending a class that has a relationship definition on it. - - e.g. - - ```ts - class Pet extends Model { - @belongsTo('human', { async: false, inverse: 'pet' }) owner; - } - class Human extends Model { - @belongsTo('pet', { async: false, inverse: 'owner' }) pet; - } - class Farmer extends Human {} - ``` - - In the above case, the following would trigger this error: - - ```ts - let pet = store.createRecord('pet'); - let farmer = store.createRecord('farmer'); - farmer.pet = pet; // error - ``` - - The correct way to fix this is to specify the polymorphic option on Pet - and to specify the abstract type 'human' on the Human base class. - - ```ts - class Pet extends Model { - @belongsTo('human', { async: false, inverse: 'pet', polymorphic: true }) owner; - } - class Human extends Model { - @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; - } - class Farmer extends Human {} - ``` - - Alternatively both Human and Farmer could declare the relationship, because relationship - definitions are "structural". - - ```ts - class Pet extends Model { - @belongsTo('human', { async: false, inverse: 'pet', polymorphic: true }) owner; - } - class Human extends Model { - @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; - } - class Farmer extends Model { - @belongsTo('pet', { async: false, inverse: 'owner', as: 'human' }) pet; - } - ``` - - */ - if (key === info.lhs_relationshipName && info.lhs_modelNames.indexOf(type) !== -1) { - // parentIdentifier, parentDefinition, addedIdentifier, store - assertInheritedSchema(info.lhs_definition, type); - } else if (key === info.rhs_relationshipName && info.rhs_modelNames.indexOf(type) !== -1) { - assertInheritedSchema(info.lhs_definition, type); - } - // OPEN AN ISSUE :: we would like to improve our errors but need to understand what corner case got us here - throw new Error( - `PLEASE OPEN AN ISSUE :: Found a relationship that is neither the LHS nor RHS of the same edge. This is not supported. Please report this to the EmberData team.` - ); - } - - if (isRHS && isLHS) { - // not sure how we get here but it's probably the result of some form of inheritance - // without having specified polymorphism correctly leading to it not being self-referential - // OPEN AN ISSUE :: we would like to improve our errors but need to understand what corner case got us here - throw new Error( - `PLEASE OPEN AN ISSUE :: Found a relationship that is both the LHS and RHS of the same edge but is not self-referential. This is not supported. Please report this to the EmberData team.` - ); - } - } -} - -export function isLHS(info: EdgeDefinition, type: string, key: string): boolean { - let isSelfReferential = info.isSelfReferential; - let isRelationship = key === info.lhs_relationshipName; - - if (DEBUG) { - assertConfiguration(info, type, key); - } - - if (isRelationship === true) { - return ( - isSelfReferential === true || // itself - type === info.lhs_baseModelName || // base or non-polymorphic - // if the other side is polymorphic then we need to scan our modelNames - (info.rhs_isPolymorphic && info.lhs_modelNames.indexOf(type) !== -1) // polymorphic - ); - } - - return false; -} - -export function isRHS(info: EdgeDefinition, type: string, key: string): boolean { - let isSelfReferential = info.isSelfReferential; - let isRelationship = key === info.rhs_relationshipName; - - if (DEBUG) { - assertConfiguration(info, type, key); - } - - if (isRelationship === true) { - return ( - isSelfReferential === true || // itself - type === info.rhs_baseModelName || // base or non-polymorphic - // if the other side is polymorphic then we need to scan our modelNames - (info.lhs_isPolymorphic && info.rhs_modelNames.indexOf(type) !== -1) // polymorphic - ); - } - - return false; -} - -export function upgradeDefinition( - graph: Graph, - identifier: StableRecordIdentifier, - propertyName: string, - isImplicit: boolean = false -): EdgeDefinition | null { - const cache = graph._definitionCache; - const storeWrapper = graph.store; - const polymorphicLookup = graph._potentialPolymorphicTypes; - - const { type } = identifier; - let cached = expandingGet(cache, type, propertyName); - - // CASE: We have a cached resolution (null if no relationship exists) - if (cached !== undefined) { - return cached; - } - - assert( - `Expected to find relationship definition in the cache for the implicit relationship ${propertyName}`, - !isImplicit - ); - - let relationships = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - assert(`Expected to have a relationship definition for ${type} but none was found.`, relationships); - let meta = relationships[propertyName]; - - if (!meta) { - // TODO potentially we should just be permissive here since this is an implicit relationship - // and not require the lookup table to be populated - if (polymorphicLookup[type]) { - const altTypes = Object.keys(polymorphicLookup[type] as {}); - for (let i = 0; i < altTypes.length; i++) { - let cached = expandingGet(cache, altTypes[i], propertyName); - if (cached) { - expandingSet(cache, type, propertyName, cached); - cached.rhs_modelNames.push(type); - return cached; - } - } - } - - // CASE: We don't have a relationship at all - // we should only hit this in prod - assert(`Expected to find a relationship definition for ${type}.${propertyName} but none was found.`, meta); - - cache[type]![propertyName] = null; - return null; - } - const definition = upgradeMeta(meta); - - let inverseDefinition; - let inverseKey; - const inverseType = definition.type; - - // CASE: Inverse is explicitly null - if (definition.inverseKey === null) { - // TODO probably dont need this assertion if polymorphic - assert(`Expected the inverse model to exist`, getStore(storeWrapper).modelFor(inverseType)); - inverseDefinition = null; - } else { - inverseKey = inverseForRelationship(getStore(storeWrapper), identifier, propertyName); - - // CASE: If we are polymorphic, and we declared an inverse that is non-null - // we must assume that the lack of inverseKey means that there is no - // concrete type as the baseType, so we must construct and artificial - // placeholder - if (!inverseKey && definition.isPolymorphic && definition.inverseKey) { - inverseDefinition = { - kind: 'belongsTo', // this must be updated when we find the first belongsTo or hasMany definition that matches - key: definition.inverseKey, - type: type, - isAsync: false, // this must be updated when we find the first belongsTo or hasMany definition that matches - isImplicit: false, - isCollection: false, // this must be updated when we find the first belongsTo or hasMany definition that matches - isPolymorphic: false, - isInitialized: false, // tracks whether we have seen the other side at least once - }; - - // CASE: Inverse resolves to null - } else if (!inverseKey) { - inverseDefinition = null; - } else { - // CASE: We have an explicit inverse or were able to resolve one - let inverseDefinitions = storeWrapper - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: inverseType }); - assert(`Expected to have a relationship definition for ${inverseType} but none was found.`, inverseDefinitions); - let meta = inverseDefinitions[inverseKey]; - assert(`Expected to find a relationship definition for ${inverseType}.${inverseKey} but none was found.`, meta); - inverseDefinition = upgradeMeta(meta); - } - } - - // CASE: We have no inverse - if (!inverseDefinition) { - // polish off meta - inverseKey = implicitKeyFor(type, propertyName); - inverseDefinition = { - kind: 'implicit', - key: inverseKey, - type: type, - isAsync: false, - isImplicit: true, - isCollection: true, // with implicits any number of records could point at us - isPolymorphic: false, - }; - - syncMeta(definition, inverseDefinition); - syncMeta(inverseDefinition, definition); - - const info = { - lhs_key: `${type}:${propertyName}`, - lhs_modelNames: [type], - lhs_baseModelName: type, - lhs_relationshipName: propertyName, - lhs_definition: definition, - lhs_isPolymorphic: definition.isPolymorphic, - - rhs_key: inverseDefinition.key, - rhs_modelNames: [inverseType], - rhs_baseModelName: inverseType, - rhs_relationshipName: inverseDefinition.key, - rhs_definition: inverseDefinition, - rhs_isPolymorphic: false, - - hasInverse: false, - isSelfReferential: type === inverseType, // this could be wrong if we are self-referential but also polymorphic - isReflexive: false, // we can't be reflexive if we don't define an inverse - }; - - expandingSet(cache, inverseType, inverseKey, info); - expandingSet(cache, type, propertyName, info); - return info; - } - - // CASE: We do have an inverse - const baseType = inverseDefinition.type; - - // TODO we want to assert this but this breaks all of our shoddily written tests - /* - if (DEBUG) { - let inverseDoubleCheck = inverseMeta.type.inverseFor(inverseRelationshipName, store); - - assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck); - } - */ - // CASE: We may have already discovered the inverse for the baseModelName - // CASE: We have already discovered the inverse - cached = expandingGet(cache, baseType, propertyName) || expandingGet(cache, inverseType, inverseKey); - - if (cached) { - // TODO this assert can be removed if the above assert is enabled - assert( - `The ${inverseType}:${inverseKey} relationship declares 'inverse: null', but it was resolved as the inverse for ${type}:${propertyName}.`, - cached.hasInverse !== false - ); - - let isLHS = cached.lhs_baseModelName === baseType; - let modelNames = isLHS ? cached.lhs_modelNames : cached.rhs_modelNames; - // make this lookup easier in the future by caching the key - modelNames.push(type); - expandingSet(cache, type, propertyName, cached); - - return cached; - } - - // this is our first time so polish off the metas - syncMeta(definition, inverseDefinition); - syncMeta(inverseDefinition, definition); - - const lhs_modelNames = [type]; - if (type !== baseType) { - lhs_modelNames.push(baseType); - } - const isSelfReferential = baseType === inverseType; - const info = { - lhs_key: `${baseType}:${propertyName}`, - lhs_modelNames, - lhs_baseModelName: baseType, - lhs_relationshipName: propertyName, - lhs_definition: definition, - lhs_isPolymorphic: definition.isPolymorphic, - - rhs_key: `${inverseType}:${inverseKey}`, - rhs_modelNames: [inverseType], - rhs_baseModelName: inverseType, - rhs_relationshipName: inverseKey, - rhs_definition: inverseDefinition, - rhs_isPolymorphic: inverseDefinition.isPolymorphic, - hasInverse: true, - isSelfReferential, - isReflexive: isSelfReferential && propertyName === inverseKey, - }; - - // Create entries for the baseModelName as well as modelName to speed up - // inverse lookups - expandingSet(cache, baseType, propertyName, info); - expandingSet(cache, type, propertyName, info); - - // Greedily populate the inverse - expandingSet(cache, inverseType, inverseKey, info); - - return info; -} - -function metaIsRelationshipDefinition(meta: RelationshipSchema): meta is RelationshipDefinition { - return typeof (meta as RelationshipDefinition)._inverseKey === 'function'; -} - -function inverseForRelationship(store: Store, identifier: StableRecordIdentifier | { type: string }, key: string) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[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 relationship defintion to specify the inverse type or null.`, - definition.options?.inverse === null || - (typeof definition.options?.inverse === 'string' && definition.options.inverse.length > 0) - ); - return definition.options.inverse; -} diff --git a/packages/graph/src/-private/graph/-operations.ts b/packages/graph/src/-private/graph/-operations.ts deleted file mode 100644 index b6ad0ea5a4f..00000000000 --- a/packages/graph/src/-private/graph/-operations.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { - CollectionResourceRelationship, - SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -export interface Operation { - op: string; -} - -export interface UpdateRelationshipOperation { - op: 'updateRelationship'; - record: StableRecordIdentifier; - field: string; - value: SingleResourceRelationship | CollectionResourceRelationship; -} - -export interface DeleteRecordOperation { - op: 'deleteRecord'; - record: StableRecordIdentifier; - isNew: boolean; -} - -export interface UnknownOperation { - op: 'never'; - record: StableRecordIdentifier; - field: string; -} - -export interface AddToRelatedRecordsOperation { - op: 'addToRelatedRecords'; - record: StableRecordIdentifier; - field: string; // "relationship" propertyName - value: StableRecordIdentifier | StableRecordIdentifier[]; // related record - index?: number; // the index to insert at -} - -export interface RemoveFromRelatedRecordsOperation { - op: 'removeFromRelatedRecords'; - record: StableRecordIdentifier; - field: string; // "relationship" propertyName - value: StableRecordIdentifier | StableRecordIdentifier[]; // related record - index?: number; // optional the index at which we're expected to start the removal -} - -export interface ReplaceRelatedRecordOperation { - op: 'replaceRelatedRecord'; - record: StableRecordIdentifier; - field: string; - value: StableRecordIdentifier | null; // never null if field is a collection - prior?: StableRecordIdentifier; // if field is a collection, the value we are swapping with - index?: number; // if field is a collection, the index at which we are replacing a value -} - -export interface SortRelatedRecords { - op: 'sortRelatedRecords'; - record: StableRecordIdentifier; - field: string; - value: StableRecordIdentifier[]; -} - -export interface ReplaceRelatedRecordsOperation { - op: 'replaceRelatedRecords'; - record: StableRecordIdentifier; - field: string; - value: StableRecordIdentifier[]; // the records to add. If no prior/index specified all existing should be removed - prior?: StableRecordIdentifier[]; // if this is a "splice" the records we expect to be removed - index?: number; // if this is a "splice" the index to start from -} - -export type RemoteRelationshipOperation = - | UpdateRelationshipOperation - | ReplaceRelatedRecordOperation - | ReplaceRelatedRecordsOperation - | DeleteRecordOperation - | SortRelatedRecords; - -export type LocalRelationshipOperation = - | ReplaceRelatedRecordsOperation - | ReplaceRelatedRecordOperation - | RemoveFromRelatedRecordsOperation - | AddToRelatedRecordsOperation - | SortRelatedRecords; diff --git a/packages/graph/src/-private/graph/-utils.ts b/packages/graph/src/-private/graph/-utils.ts deleted file mode 100644 index ff9f325ed3d..00000000000 --- a/packages/graph/src/-private/graph/-utils.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { assert, inspect, warn } from '@ember/debug'; - -import { LOG_GRAPH } from '@ember-data/debugging'; -import type { Store } from '@ember-data/store/-private'; -import { peekCache } from '@ember-data/store/-private'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { Dict } from '@ember-data/types/q/utils'; - -import { coerceId } from '../coerce-id'; -import type BelongsToRelationship from '../relationships/state/belongs-to'; -import type ManyRelationship from '../relationships/state/has-many'; -import type { UpdateRelationshipOperation } from './-operations'; -import type { Graph, ImplicitRelationship } from './graph'; - -export function getStore(wrapper: CacheStoreWrapper | { _store: Store }): Store { - assert(`expected a private _store property`, '_store' in wrapper); - return wrapper._store; -} - -export function expandingGet(cache: Dict>, key1: string, key2: string): T | undefined { - let mainCache = (cache[key1] = cache[key1] || Object.create(null)); - return mainCache[key2]; -} - -export function expandingSet(cache: Dict>, key1: string, key2: string, value: T): void { - let mainCache = (cache[key1] = cache[key1] || Object.create(null)); - mainCache[key2] = value; -} - -export function assertValidRelationshipPayload(graph: Graph, op: UpdateRelationshipOperation) { - const relationship = graph.get(op.record, op.field); - assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); - const payload = op.value; - const { definition, identifier, state } = relationship; - const { type } = identifier; - const { field } = op; - const { isAsync, kind } = definition; - - if (payload.links) { - warn( - `You pushed a record of type '${type}' with a relationship '${field}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, - isAsync || !!payload.data || state.hasReceivedData, - { - id: 'ds.store.push-link-for-sync-relationship', - } - ); - } else if (payload.data) { - if (kind === 'belongsTo') { - assert( - `A ${type} record was pushed into the store with the value of ${field} being ${inspect( - payload.data - )}, but ${field} is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, - !Array.isArray(payload.data) - ); - assertRelationshipData(getStore(graph.store), identifier, payload.data, definition); - } else if (kind === 'hasMany') { - assert( - `A ${type} record was pushed into the store with the value of ${field} being '${inspect( - payload.data - )}', but ${field} is a hasMany relationship so the value must be an array. You should probably check your data payload or serializer.`, - Array.isArray(payload.data) - ); - if (Array.isArray(payload.data)) { - for (let i = 0; i < payload.data.length; i++) { - assertRelationshipData(getStore(graph.store), identifier, payload.data[i], definition); - } - } - } - } -} - -export function isNew(identifier: StableRecordIdentifier): boolean { - if (!identifier.id) { - return true; - } - const cache = peekCache(identifier); - return Boolean(cache?.isNew(identifier)); -} - -export function isBelongsTo( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is BelongsToRelationship { - return relationship.definition.kind === 'belongsTo'; -} - -export function isImplicit( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is ImplicitRelationship { - return relationship.definition.isImplicit; -} - -export function isHasMany( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is ManyRelationship { - return relationship.definition.kind === 'hasMany'; -} - -export function forAllRelatedIdentifiers( - rel: BelongsToRelationship | ManyRelationship | ImplicitRelationship, - cb: (identifier: StableRecordIdentifier) => void -): void { - if (isBelongsTo(rel)) { - if (rel.remoteState) { - cb(rel.remoteState); - } - if (rel.localState && rel.localState !== rel.remoteState) { - cb(rel.localState); - } - } else if (isHasMany(rel)) { - // ensure we don't walk anything twice if an entry is - // in both localMembers and remoteMembers - let seen = new Set(); - - for (let i = 0; i < rel.localState.length; i++) { - const inverseIdentifier = rel.localState[i]; - if (!seen.has(inverseIdentifier)) { - seen.add(inverseIdentifier); - cb(inverseIdentifier); - } - } - - for (let i = 0; i < rel.remoteState.length; i++) { - const inverseIdentifier = rel.remoteState[i]; - if (!seen.has(inverseIdentifier)) { - seen.add(inverseIdentifier); - cb(inverseIdentifier); - } - } - } else { - let seen = new Set(); - rel.localMembers.forEach((inverseIdentifier) => { - if (!seen.has(inverseIdentifier)) { - seen.add(inverseIdentifier); - cb(inverseIdentifier); - } - }); - rel.remoteMembers.forEach((inverseIdentifier) => { - if (!seen.has(inverseIdentifier)) { - seen.add(inverseIdentifier); - cb(inverseIdentifier); - } - }); - } -} - -/* - Removes the given identifier from BOTH remote AND local state. - - This method is useful when either a deletion or a rollback on a new record - needs to entirely purge itself from an inverse relationship. - */ -export function removeIdentifierCompletelyFromRelationship( - graph: Graph, - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship, - value: StableRecordIdentifier, - silenceNotifications?: boolean -): void { - if (isBelongsTo(relationship)) { - if (relationship.remoteState === value) { - relationship.remoteState = null; - } - - if (relationship.localState === value) { - relationship.localState = null; - // This allows dematerialized inverses to be rematerialized - // we shouldn't be notifying here though, figure out where - // a notification was missed elsewhere. - if (!silenceNotifications) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } - } else if (isHasMany(relationship)) { - relationship.remoteMembers.delete(value); - relationship.localMembers.delete(value); - - const canonicalIndex = relationship.remoteState.indexOf(value); - if (canonicalIndex !== -1) { - relationship.remoteState.splice(canonicalIndex, 1); - } - - const currentIndex = relationship.localState.indexOf(value); - if (currentIndex !== -1) { - relationship.localState.splice(currentIndex, 1); - // This allows dematerialized inverses to be rematerialized - // we shouldn't be notifying here though, figure out where - // a notification was missed elsewhere. - if (!silenceNotifications) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } - } else { - relationship.remoteMembers.delete(value); - relationship.localMembers.delete(value); - } -} - -// 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 - console.log(`Graph: ignoring relationship change for removed identifier ${String(identifier)} ${key}`); - } - return; - } - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`Graph: notifying relationship change for ${String(identifier)} ${key}`); - } - - graph.store.notifyChange(identifier, 'relationships', key); -} - -export function assertRelationshipData(store, identifier, data, meta) { - assert( - `A ${identifier.type} record was pushed into the store with the value of ${meta.key} being '${JSON.stringify( - data - )}', but ${ - meta.key - } is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`, - !Array.isArray(data) - ); - assert( - `Encountered a relationship identifier without a type for the ${meta.kind} relationship '${meta.key}' on <${ - identifier.type - }:${identifier.id}>, expected an identifier with type '${meta.type}' but found\n\n'${JSON.stringify( - data, - null, - 2 - )}'\n\nPlease check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, - data === null || (typeof data.type === 'string' && data.type.length) - ); - assert( - `Encountered a relationship identifier without an id for the ${meta.kind} relationship '${meta.key}' on <${ - identifier.type - }:${identifier.id}>, expected an identifier but found\n\n'${JSON.stringify( - data, - null, - 2 - )}'\n\nPlease check your serializer and make sure it is serializing the relationship payload into a JSON API format.`, - data === null || !!coerceId(data.id) - ); - assert( - `Encountered a relationship identifier with type '${data.type}' for the ${meta.kind} relationship '${meta.key}' on <${identifier.type}:${identifier.id}>, Expected an identifier with type '${meta.type}'. No model was found for '${data.type}'.`, - data === null || !data.type || store.getSchemaDefinitionService().doesTypeExist(data.type) - ); -} diff --git a/packages/graph/src/-private/graph/graph.ts b/packages/graph/src/-private/graph/graph.ts deleted file mode 100644 index 99a8585e21e..00000000000 --- a/packages/graph/src/-private/graph/graph.ts +++ /dev/null @@ -1,617 +0,0 @@ -import { assert } from '@ember/debug'; - -import { LOG_GRAPH } from '@ember-data/debugging'; -import { DEBUG } from '@ember-data/env'; -import { MergeOperation } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { Dict } from '@ember-data/types/q/utils'; - -import BelongsToRelationship from '../relationships/state/belongs-to'; -import ManyRelationship from '../relationships/state/has-many'; -import type { EdgeCache, UpgradedMeta } from './-edge-definition'; -import { isLHS, upgradeDefinition } from './-edge-definition'; -import type { - DeleteRecordOperation, - LocalRelationshipOperation, - RemoteRelationshipOperation, - UnknownOperation, -} from './-operations'; -import { - assertValidRelationshipPayload, - forAllRelatedIdentifiers, - getStore, - isBelongsTo, - isHasMany, - isImplicit, - isNew, - notifyChange, - removeIdentifierCompletelyFromRelationship, -} from './-utils'; -import addToRelatedRecords from './operations/add-to-related-records'; -import { mergeIdentifier } from './operations/merge-identifier'; -import removeFromRelatedRecords from './operations/remove-from-related-records'; -import replaceRelatedRecord from './operations/replace-related-record'; -import replaceRelatedRecords, { syncRemoteToLocal } from './operations/replace-related-records'; -import updateRelationshipOperation from './operations/update-relationship'; - -export interface ImplicitRelationship { - definition: UpgradedMeta; - identifier: StableRecordIdentifier; - localMembers: Set; - remoteMembers: Set; -} - -export type RelationshipEdge = ImplicitRelationship | ManyRelationship | BelongsToRelationship; - -export const Graphs = new Map(); - -/* - * Graph acts as the cache for relationship data. It allows for - * us to ask about and update relationships for a given Identifier - * without requiring other objects for that Identifier to be - * instantiated (such as `RecordData` or a `Record`) - * - * This also allows for us to make more substantive changes to relationships - * with increasingly minor alterations to other portions of the internals - * over time. - * - * The graph is made up of nodes and edges. Each unique identifier gets - * its own node, which is a dictionary with a list of that node's edges - * (or connections) to other nodes. In `Model` terms, a node represents a - * record instance, with each key (an edge) in the dictionary correlating - * to either a `hasMany` or `belongsTo` field on that record instance. - * - * The value for each key, or `edge` is the identifier(s) the node relates - * to in the graph from that key. - */ -export class Graph { - declare _definitionCache: EdgeCache; - declare _potentialPolymorphicTypes: Dict>; - declare identifiers: Map>; - declare store: CacheStoreWrapper; - declare isDestroyed: boolean; - declare _willSyncRemote: boolean; - declare _willSyncLocal: boolean; - declare _pushedUpdates: { - belongsTo: RemoteRelationshipOperation[]; - hasMany: RemoteRelationshipOperation[]; - deletions: DeleteRecordOperation[]; - }; - declare _updatedRelationships: Set; - declare _transaction: Set | null; - declare _removing: StableRecordIdentifier | null; - - constructor(store: CacheStoreWrapper) { - this._definitionCache = Object.create(null) as EdgeCache; - this._potentialPolymorphicTypes = Object.create(null) as Dict>; - this.identifiers = new Map(); - this.store = store; - this.isDestroyed = false; - this._willSyncRemote = false; - this._willSyncLocal = false; - this._pushedUpdates = { belongsTo: [], hasMany: [], deletions: [] }; - this._updatedRelationships = new Set(); - this._transaction = null; - this._removing = null; - } - - has(identifier: StableRecordIdentifier, propertyName: string): boolean { - let relationships = this.identifiers.get(identifier); - if (!relationships) { - return false; - } - return relationships[propertyName] !== undefined; - } - - get(identifier: StableRecordIdentifier, propertyName: string): RelationshipEdge { - assert(`expected propertyName`, propertyName); - let relationships = this.identifiers.get(identifier); - if (!relationships) { - relationships = Object.create(null) as Dict; - this.identifiers.set(identifier, relationships); - } - - let relationship = relationships[propertyName]; - if (!relationship) { - const info = upgradeDefinition(this, identifier, propertyName); - assert(`Could not determine relationship information for ${identifier.type}.${propertyName}`, info !== null); - - // if (info.rhs_definition?.kind === 'implicit') { - // // we should possibly also do this - // // but it would result in being extremely permissive for other relationships by accident - // // this.registerPolymorphicType(info.rhs_baseModelName, identifier.type); - // } - - const meta = isLHS(info, identifier.type, propertyName) ? info.lhs_definition : info.rhs_definition!; - - if (meta.kind !== 'implicit') { - const Klass = meta.kind === 'hasMany' ? ManyRelationship : BelongsToRelationship; - relationship = relationships[propertyName] = new Klass(meta, identifier); - } else { - relationship = relationships[propertyName] = { - definition: meta, - identifier, - localMembers: new Set(), - remoteMembers: new Set(), - }; - } - } - - return relationship; - } - - /* - * Allows for the graph to dynamically discover polymorphic connections - * without needing to walk prototype chains. - * - * Used by edges when an added `type` does not match the expected `type` - * for that edge. - * - * Currently we assert before calling this. For a public API we will want - * to call out to the schema manager to ask if we should consider these - * types as equivalent for a given relationship. - */ - registerPolymorphicType(type1: string, type2: string): void { - const typeCache = this._potentialPolymorphicTypes; - let t1 = typeCache[type1]; - if (!t1) { - t1 = typeCache[type1] = Object.create(null) as Dict; - } - t1[type2] = true; - - let t2 = typeCache[type2]; - if (!t2) { - t2 = typeCache[type2] = Object.create(null) as Dict; - } - t2[type1] = true; - } - - /* - TODO move this comment somewhere else - implicit relationships are relationships which have not been declared but the inverse side exists on - another record somewhere - - For example if there was: - - ```app/models/comment.js - import Model, { attr } from '@ember-data/model'; - - export default class Comment extends Model { - @attr text; - } - ``` - - and there is also: - - ```app/models/post.js - import Model, { attr, hasMany } from '@ember-data/model'; - - export default class Post extends Model { - @attr title; - @hasMany('comment', { async: true, inverse: null }) comments; - } - ``` - - Then we would have a implicit 'post' relationship for the comment record in order - to be do things like remove the comment from the post if the comment were to be deleted. - */ - - isReleasable(identifier: StableRecordIdentifier): boolean { - const relationships = this.identifiers.get(identifier); - if (!relationships) { - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: RELEASABLE ${String(identifier)}`); - } - return true; - } - const keys = Object.keys(relationships); - for (let i = 0; i < keys.length; i++) { - const relationship = relationships[keys[i]] as RelationshipEdge; - // account for previously unloaded relationships - // typically from a prior deletion of a record that pointed to this one implicitly - if (relationship === undefined) { - continue; - } - assert(`Expected a relationship`, relationship); - if (relationship.definition.inverseIsAsync) { - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: <> RELEASABLE ${String(identifier)}`); - } - return false; - } - } - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: RELEASABLE ${String(identifier)}`); - } - return true; - } - - unload(identifier: StableRecordIdentifier, silenceNotifications?: boolean) { - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: unload ${String(identifier)}`); - } - const relationships = this.identifiers.get(identifier); - - if (relationships) { - // cleans up the graph but retains some nodes - // to allow for rematerialization - Object.keys(relationships).forEach((key) => { - let rel = relationships[key]!; - if (!rel) { - return; - } - destroyRelationship(this, rel, silenceNotifications); - if (isImplicit(rel)) { - relationships[key] = undefined; - } - }); - } - } - - remove(identifier: StableRecordIdentifier) { - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: remove ${String(identifier)}`); - } - assert(`Cannot remove ${String(identifier)} while still removing ${String(this._removing)}`, !this._removing); - this._removing = identifier; - this.unload(identifier); - this.identifiers.delete(identifier); - this._removing = null; - } - - /* - * Remote state changes - */ - push(op: RemoteRelationshipOperation) { - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: push ${String(op.record)}`, op); - } - if (op.op === 'deleteRecord') { - this._pushedUpdates.deletions.push(op); - } else if (op.op === 'replaceRelatedRecord') { - this._pushedUpdates.belongsTo.push(op); - } else { - const relationship = this.get(op.record, op.field); - assert(`Cannot push a remote update for an implicit relationship`, !isImplicit(relationship)); - this._pushedUpdates[relationship.definition.kind as 'belongsTo' | 'hasMany'].push(op); - } - if (!this._willSyncRemote) { - this._willSyncRemote = true; - getStore(this.store)._schedule('coalesce', () => this._flushRemoteQueue()); - } - } - - /* - * Local state changes - */ - update(op: RemoteRelationshipOperation | MergeOperation, isRemote: true): void; - update(op: LocalRelationshipOperation, isRemote?: false): void; - update( - op: MergeOperation | LocalRelationshipOperation | RemoteRelationshipOperation | UnknownOperation, - isRemote: boolean = false - ): void { - assert( - `Cannot update an implicit relationship`, - op.op === 'deleteRecord' || op.op === 'mergeIdentifiers' || !isImplicit(this.get(op.record, op.field)) - ); - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`graph: update (${isRemote ? 'remote' : 'local'}) ${String(op.record)}`, op); - } - - switch (op.op) { - case 'mergeIdentifiers': { - const relationships = this.identifiers.get(op.record); - if (relationships) { - mergeIdentifier(this, op, relationships); - } - break; - } - case 'updateRelationship': - assert(`Can only perform the operation updateRelationship on remote state`, isRemote); - if (DEBUG) { - // in debug, assert payload validity eagerly - // TODO add deprecations/assertion here for duplicates - assertValidRelationshipPayload(this, op); - } - updateRelationshipOperation(this, op); - break; - case 'deleteRecord': { - assert(`Can only perform the operation deleteRelationship on remote state`, isRemote); - const identifier = op.record; - const relationships = this.identifiers.get(identifier); - - if (relationships) { - Object.keys(relationships).forEach((key) => { - const rel = relationships[key]; - if (!rel) { - return; - } - // works together with the has check - relationships[key] = undefined; - removeCompletelyFromInverse(this, rel); - }); - this.identifiers.delete(identifier); - } - break; - } - case 'replaceRelatedRecord': - replaceRelatedRecord(this, op, isRemote); - break; - case 'addToRelatedRecords': - addToRelatedRecords(this, op, isRemote); - break; - case 'removeFromRelatedRecords': - removeFromRelatedRecords(this, op, isRemote); - break; - case 'replaceRelatedRecords': - replaceRelatedRecords(this, op, isRemote); - break; - default: - assert(`No local relationship update operation exists for '${op.op}'`); - } - } - - _scheduleLocalSync(relationship: ManyRelationship) { - this._updatedRelationships.add(relationship); - if (!this._willSyncLocal) { - this._willSyncLocal = true; - getStore(this.store)._schedule('sync', () => this._flushLocalQueue()); - } - } - - _flushRemoteQueue() { - if (!this._willSyncRemote) { - return; - } - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.groupCollapsed(`Graph: Initialized Transaction`); - } - this._transaction = new Set(); - this._willSyncRemote = false; - const { deletions, hasMany, belongsTo } = this._pushedUpdates; - this._pushedUpdates.deletions = []; - this._pushedUpdates.hasMany = []; - this._pushedUpdates.belongsTo = []; - - for (let i = 0; i < deletions.length; i++) { - this.update(deletions[i], true); - } - - for (let i = 0; i < hasMany.length; i++) { - this.update(hasMany[i], true); - } - - for (let i = 0; i < belongsTo.length; i++) { - this.update(belongsTo[i], true); - } - this._finalize(); - } - - _addToTransaction(relationship: ManyRelationship | BelongsToRelationship) { - assert(`expected a transaction`, this._transaction !== null); - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`Graph: ${String(relationship.identifier)} ${relationship.definition.key} added to transaction`); - } - relationship.transactionRef++; - this._transaction.add(relationship); - } - - _finalize() { - if (this._transaction) { - this._transaction.forEach((v) => (v.transactionRef = 0)); - this._transaction = null; - if (LOG_GRAPH) { - // eslint-disable-next-line no-console - console.log(`Graph: transaction finalized`); - // eslint-disable-next-line no-console - console.groupEnd(); - } - } - } - - _flushLocalQueue() { - if (!this._willSyncLocal) { - return; - } - this._willSyncLocal = false; - let updated = this._updatedRelationships; - this._updatedRelationships = new Set(); - updated.forEach((rel) => syncRemoteToLocal(this, rel)); - } - - destroy() { - Graphs.delete(this.store); - - if (DEBUG) { - Graphs.delete(getStore(this.store) as unknown as CacheStoreWrapper); - if (Graphs.size) { - Graphs.forEach((_, key) => { - assert( - `Memory Leak Detected, likely the test or app instance previous to this was not torn down properly`, - // @ts-expect-error - !key.isDestroyed && !key.isDestroying - ); - }); - } - } - - this.identifiers.clear(); - this.store = null as unknown as CacheStoreWrapper; - this.isDestroyed = true; - } -} - -// Handle dematerialization for relationship `rel`. In all cases, notify the -// relationship of the dematerialization: this is done so the relationship can -// notify its inverse which needs to update state -// -// If the inverse is sync, unloading this record is treated as a client-side -// delete, so we remove the inverse records from this relationship to -// disconnect the graph. Because it's not async, we don't need to keep around -// the identifier as an id-wrapper for references -function destroyRelationship(graph: Graph, rel: RelationshipEdge, silenceNotifications?: boolean) { - if (isImplicit(rel)) { - if (graph.isReleasable(rel.identifier)) { - removeCompletelyFromInverse(graph, rel); - } - return; - } - - const { identifier } = rel; - const { inverseKey } = rel.definition; - - if (!rel.definition.inverseIsImplicit) { - forAllRelatedIdentifiers(rel, (inverseIdentifer: StableRecordIdentifier) => - notifyInverseOfDematerialization(graph, inverseIdentifer, inverseKey, identifier, silenceNotifications) - ); - } - - if (!rel.definition.inverseIsImplicit && !rel.definition.inverseIsAsync) { - rel.state.isStale = true; - clearRelationship(rel); - - // necessary to clear relationships in the ui from dematerialized records - // hasMany is managed by Model which calls `retreiveLatest` after - // dematerializing the resource-cache instance. - // but sync belongsTo requires this since they don't have a proxy to update. - // so we have to notify so it will "update" to null. - // we should discuss whether we still care about this, probably fine to just - // leave the ui relationship populated since the record is destroyed and - // internally we've fully cleaned up. - if (!rel.definition.isAsync && !silenceNotifications) { - notifyChange(graph, rel.identifier, rel.definition.key); - } - } -} - -function notifyInverseOfDematerialization( - graph: Graph, - inverseIdentifier: StableRecordIdentifier, - inverseKey: string, - identifier: StableRecordIdentifier, - silenceNotifications?: boolean -) { - if (!graph.has(inverseIdentifier, inverseKey)) { - return; - } - - let relationship = graph.get(inverseIdentifier, inverseKey); - assert(`expected no implicit`, !isImplicit(relationship)); - - // For remote members, it is possible that inverseRecordData has already been associated to - // to another record. For such cases, do not dematerialize the inverseRecordData - if (!isBelongsTo(relationship) || !relationship.localState || identifier === relationship.localState) { - removeDematerializedInverse( - graph, - relationship as BelongsToRelationship | ManyRelationship, - identifier, - silenceNotifications - ); - } -} - -function clearRelationship(relationship: ManyRelationship | BelongsToRelationship) { - if (isBelongsTo(relationship)) { - relationship.localState = null; - relationship.remoteState = null; - relationship.state.hasReceivedData = false; - relationship.state.isEmpty = true; - } else { - relationship.localMembers.clear(); - relationship.remoteMembers.clear(); - relationship.localState = []; - relationship.remoteState = []; - } -} - -function removeDematerializedInverse( - graph: Graph, - relationship: ManyRelationship | BelongsToRelationship, - inverseIdentifier: StableRecordIdentifier, - silenceNotifications?: boolean -) { - if (isBelongsTo(relationship)) { - const inverseIdentifier = relationship.localState; - if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) { - // unloading inverse of a sync relationship is treated as a client-side - // delete, so actually remove the models don't merely invalidate the cp - // cache. - // if the record being unloaded only exists on the client, we similarly - // treat it as a client side delete - if (relationship.localState === inverseIdentifier && inverseIdentifier !== null) { - relationship.localState = null; - } - - if (relationship.remoteState === inverseIdentifier && inverseIdentifier !== null) { - relationship.remoteState = null; - relationship.state.hasReceivedData = true; - relationship.state.isEmpty = true; - if (relationship.localState && !isNew(relationship.localState)) { - relationship.localState = null; - } - } - } else { - relationship.state.hasDematerializedInverse = true; - } - - if (!silenceNotifications) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } else { - if (!relationship.definition.isAsync || (inverseIdentifier && isNew(inverseIdentifier))) { - // unloading inverse of a sync relationship is treated as a client-side - // delete, so actually remove the models don't merely invalidate the cp - // cache. - // if the record being unloaded only exists on the client, we similarly - // treat it as a client side delete - removeIdentifierCompletelyFromRelationship(graph, relationship, inverseIdentifier); - } else { - relationship.state.hasDematerializedInverse = true; - } - - if (!silenceNotifications) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } -} - -function removeCompletelyFromInverse( - graph: Graph, - relationship: ImplicitRelationship | ManyRelationship | BelongsToRelationship -) { - const { identifier } = relationship; - const { inverseKey } = relationship.definition; - - forAllRelatedIdentifiers(relationship, (inverseIdentifier: StableRecordIdentifier) => { - if (graph.has(inverseIdentifier, inverseKey)) { - removeIdentifierCompletelyFromRelationship(graph, graph.get(inverseIdentifier, inverseKey), identifier); - } - }); - - if (isBelongsTo(relationship)) { - if (!relationship.definition.isAsync) { - clearRelationship(relationship); - } - - relationship.localState = null; - } else if (isHasMany(relationship)) { - if (!relationship.definition.isAsync) { - clearRelationship(relationship); - - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } else { - relationship.remoteMembers.clear(); - relationship.localMembers.clear(); - } -} diff --git a/packages/graph/src/-private/graph/index.ts b/packages/graph/src/-private/graph/index.ts deleted file mode 100644 index ad0e3067983..00000000000 --- a/packages/graph/src/-private/graph/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { DEBUG } from '@ember-data/env'; -import type Store from '@ember-data/store'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import type { UpgradedMeta } from './-edge-definition'; -import { getStore } from './-utils'; -import { Graph, Graphs } from './graph'; - -export interface ImplicitRelationship { - definition: UpgradedMeta; - identifier: StableRecordIdentifier; - localMembers: Set; - remoteMembers: Set; -} - -function isStore(maybeStore: unknown): maybeStore is Store { - return (maybeStore as Store)._instanceCache !== undefined; -} - -function getWrapper(store: CacheStoreWrapper | Store): CacheStoreWrapper { - return isStore(store) ? store._instanceCache._storeWrapper : store; -} - -export function peekGraph(store: CacheStoreWrapper | Store): Graph | undefined { - return Graphs.get(getWrapper(store)); -} -export type peekGraph = typeof peekGraph; - -export function graphFor(store: CacheStoreWrapper | Store): Graph { - const wrapper = getWrapper(store); - let graph = Graphs.get(wrapper); - - if (!graph) { - graph = new Graph(wrapper); - Graphs.set(wrapper, graph); - - // in DEBUG we attach the graph to the main store for improved debuggability - if (DEBUG) { - if (getStore(wrapper).isDestroying) { - throw new Error(`Memory Leak Detected During Teardown`); - } - Graphs.set(getStore(wrapper) as unknown as CacheStoreWrapper, graph); - } - } - return graph; -} diff --git a/packages/graph/src/-private/graph/operations/add-to-related-records.ts b/packages/graph/src/-private/graph/operations/add-to-related-records.ts deleted file mode 100644 index 54ecae501d3..00000000000 --- a/packages/graph/src/-private/graph/operations/add-to-related-records.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { assert } from '@ember/debug'; - -import { DEBUG } from '@ember-data/env'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { assertPolymorphicType } from '../../debug/assert-polymorphic-type'; -import type ManyRelationship from '../../relationships/state/has-many'; -import type { AddToRelatedRecordsOperation } from '../-operations'; -import { isHasMany, notifyChange } from '../-utils'; -import type { Graph } from '../graph'; -import { addToInverse } from './replace-related-records'; - -export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecordsOperation, isRemote: boolean) { - const { record, value, index } = op; - const relationship = graph.get(record, op.field); - assert( - `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) - ); - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - addRelatedRecord(graph, relationship, record, value[i], index !== undefined ? index + i : index, isRemote); - } - } else { - addRelatedRecord(graph, relationship, record, value, index, isRemote); - } - - notifyChange(graph, relationship.identifier, relationship.definition.key); -} - -function addRelatedRecord( - graph: Graph, - relationship: ManyRelationship, - record: StableRecordIdentifier, - value: StableRecordIdentifier, - index: number | undefined, - isRemote: boolean -) { - assert(`expected an identifier to add to the relationship`, value); - const { localMembers, localState } = relationship; - - if (localMembers.has(value)) { - return; - } - - const { type } = relationship.definition; - if (type !== value.type) { - if (DEBUG) { - assertPolymorphicType(record, relationship.definition, value, graph.store); - } - graph.registerPolymorphicType(value.type, type); - } - - relationship.state.hasReceivedData = true; - localMembers.add(value); - if (index === undefined) { - localState.push(value); - } else { - localState.splice(index, 0, value); - } - - addToInverse(graph, value, relationship.definition.inverseKey, record, isRemote); -} diff --git a/packages/graph/src/-private/graph/operations/merge-identifier.ts b/packages/graph/src/-private/graph/operations/merge-identifier.ts deleted file mode 100644 index e9bb1b65616..00000000000 --- a/packages/graph/src/-private/graph/operations/merge-identifier.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { MergeOperation } from '@ember-data/types/q/cache'; -import type { Dict } from '@ember-data/types/q/utils'; - -import type BelongsToRelationship from '../../relationships/state/belongs-to'; -import type ManyRelationship from '../../relationships/state/has-many'; -import { forAllRelatedIdentifiers, isBelongsTo, isHasMany, notifyChange } from '../-utils'; -import type { Graph, ImplicitRelationship, RelationshipEdge } from '../graph'; - -export function mergeIdentifier(graph: Graph, op: MergeOperation, relationships: Dict) { - Object.keys(relationships).forEach((key) => { - const rel = relationships[key]; - if (!rel) { - return; - } - mergeIdentifierForRelationship(graph, op, rel); - }); -} - -function mergeIdentifierForRelationship(graph: Graph, op: MergeOperation, rel: RelationshipEdge): void { - rel.identifier = op.value; - forAllRelatedIdentifiers(rel, (identifier) => { - const inverse = graph.get(identifier, rel.definition.inverseKey); - mergeInRelationship(graph, inverse, op); - }); -} - -function mergeInRelationship(graph: Graph, rel: RelationshipEdge, op: MergeOperation): void { - if (isBelongsTo(rel)) { - mergeBelongsTo(graph, rel, op); - } else if (isHasMany(rel)) { - mergeHasMany(graph, rel, op); - } else { - mergeImplicit(graph, rel, op); - } -} - -function mergeBelongsTo(graph: Graph, rel: BelongsToRelationship, op: MergeOperation): void { - if (rel.remoteState === op.record) { - rel.remoteState = op.value; - } - if (rel.localState === op.record) { - rel.localState = op.value; - notifyChange(graph, rel.identifier, rel.definition.key); - } -} - -function mergeHasMany(graph: Graph, rel: ManyRelationship, op: MergeOperation): void { - if (rel.remoteMembers.has(op.record)) { - rel.remoteMembers.delete(op.record); - rel.remoteMembers.add(op.value); - const index = rel.remoteState.indexOf(op.record); - rel.remoteState.splice(index, 1, op.value); - } - if (rel.localMembers.has(op.record)) { - rel.localMembers.delete(op.record); - rel.localMembers.add(op.value); - const index = rel.localState.indexOf(op.record); - rel.localState.splice(index, 1, op.value); - notifyChange(graph, rel.identifier, rel.definition.key); - } -} - -function mergeImplicit(graph: Graph, rel: ImplicitRelationship, op: MergeOperation): void { - if (rel.remoteMembers.has(op.record)) { - rel.remoteMembers.delete(op.record); - rel.remoteMembers.add(op.value); - } - if (rel.localMembers.has(op.record)) { - rel.localMembers.delete(op.record); - rel.localMembers.add(op.value); - } -} diff --git a/packages/graph/src/-private/graph/operations/remove-from-related-records.ts b/packages/graph/src/-private/graph/operations/remove-from-related-records.ts deleted file mode 100644 index 700b155ac81..00000000000 --- a/packages/graph/src/-private/graph/operations/remove-from-related-records.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { assert } from '@ember/debug'; - -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import type ManyRelationship from '../../relationships/state/has-many'; -import type { RemoveFromRelatedRecordsOperation } from '../-operations'; -import { isHasMany, notifyChange } from '../-utils'; -import type { Graph } from '../graph'; -import { removeFromInverse } from './replace-related-records'; - -export default function removeFromRelatedRecords( - graph: Graph, - op: RemoveFromRelatedRecordsOperation, - isRemote: boolean -) { - const { record, value } = op; - const relationship = graph.get(record, op.field); - assert( - `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) - ); - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - removeRelatedRecord(graph, relationship, record, value[i], isRemote); - } - } else { - removeRelatedRecord(graph, relationship, record, value, isRemote); - } - notifyChange(graph, relationship.identifier, relationship.definition.key); -} - -function removeRelatedRecord( - graph: Graph, - relationship: ManyRelationship, - record: StableRecordIdentifier, - value: StableRecordIdentifier, - isRemote: boolean -) { - assert(`expected an identifier to add to the relationship`, value); - const { localMembers, localState } = relationship; - - if (!localMembers.has(value)) { - return; - } - - localMembers.delete(value); - let index = localState.indexOf(value); - - assert(`expected localMembers and localState to be in sync`, index !== -1); - localState.splice(index, 1); - - removeFromInverse(graph, value, relationship.definition.inverseKey, record, isRemote); -} diff --git a/packages/graph/src/-private/graph/operations/replace-related-record.ts b/packages/graph/src/-private/graph/operations/replace-related-record.ts deleted file mode 100644 index a7392d4aaba..00000000000 --- a/packages/graph/src/-private/graph/operations/replace-related-record.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { assert } from '@ember/debug'; - -import { DEBUG } from '@ember-data/env'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { assertPolymorphicType } from '../../debug/assert-polymorphic-type'; -import type { ReplaceRelatedRecordOperation } from '../-operations'; -import { isBelongsTo, isNew, notifyChange } from '../-utils'; -import type { Graph } from '../graph'; -import { addToInverse, notifyInverseOfPotentialMaterialization, removeFromInverse } from './replace-related-records'; - -export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRecordOperation, isRemote = false) { - const relationship = graph.get(op.record, op.field); - assert( - `You can only '${op.op}' on a belongsTo relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, - isBelongsTo(relationship) - ); - if (isRemote) { - graph._addToTransaction(relationship); - } - const { definition, state } = relationship; - const prop = isRemote ? 'remoteState' : 'localState'; - const existingState: StableRecordIdentifier | null = relationship[prop]; - - /* - case 1:1 - ======== - In a bi-directional graph with 1:1 edges, replacing a value - results in up-to 4 discrete value transitions. - - If: A <-> B, C <-> D is the initial state, - and: A <-> C, B, D is the final state - - then we would undergo the following 4 transitions. - - remove A from B - add C to A - remove C from D - add A to C - - case 1:many - =========== - In a bi-directional graph with 1:Many edges, replacing a value - results in up-to 3 discrete value transitions. - - If: A<->>B<<->D, C<<->D is the initial state (double arrows representing the many side) - And: A<->>C<<->D, B<<->D is the final state - - Then we would undergo three transitions. - - remove A from B - add C to A. - add A to C - - case 1:? - ======== - In a uni-directional graph with 1:? edges (modeled in EmberData with `inverse:null`) with - artificial (implicit) inverses, replacing a value results in up-to 3 discrete value transitions. - This is because a 1:? relationship is effectively 1:many. - - If: A->B, C->B is the initial state - And: A->C, C->B is the final state - - Then we would undergo three transitions. - - Remove A from B - Add C to A - Add A to C - */ - - // nothing for us to do - if (op.value === existingState) { - // if we were empty before but now know we are empty this needs to be true - state.hasReceivedData = true; - // if this is a remote update we still sync - if (isRemote) { - const { localState } = relationship; - // don't sync if localState is a new record and our remoteState is null - if (localState && isNew(localState) && !existingState) { - return; - } - if (existingState && localState === existingState) { - notifyInverseOfPotentialMaterialization(graph, existingState, definition.inverseKey, op.record, isRemote); - } else { - relationship.localState = existingState; - - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } - return; - } - - // remove this value from the inverse if required - if (existingState) { - removeFromInverse(graph, existingState, definition.inverseKey, op.record, isRemote); - } - - // update value to the new value - relationship[prop] = op.value; - state.hasReceivedData = true; - state.isEmpty = op.value === null; - state.isStale = false; - state.hasFailedLoadAttempt = false; - - if (op.value) { - if (definition.type !== op.value.type) { - // assert( - // `The '<${definition.inverseType}>.${op.field}' relationship expects only '${definition.type}' records since it is not polymorphic. Received a Record of type '${op.value.type}'`, - // definition.isPolymorphic - // ); - - // TODO this should now handle the deprecation warning if isPolymorphic is not set - // but the record does turn out to be polymorphic - // this should still assert if the user is relying on legacy inheritance/mixins to - // provide polymorphic behavior and has not yet added the polymorphic flags - if (DEBUG) { - assertPolymorphicType(relationship.identifier, definition, op.value, graph.store); - } - - graph.registerPolymorphicType(definition.type, op.value.type); - } - addToInverse(graph, op.value, definition.inverseKey, op.record, isRemote); - } - - if (isRemote) { - const { localState, remoteState } = relationship; - if (localState && isNew(localState) && !remoteState) { - return; - } - if (localState !== remoteState) { - relationship.localState = remoteState; - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } else { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } -} diff --git a/packages/graph/src/-private/graph/operations/replace-related-records.ts b/packages/graph/src/-private/graph/operations/replace-related-records.ts deleted file mode 100644 index bd561aee4e2..00000000000 --- a/packages/graph/src/-private/graph/operations/replace-related-records.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { assert } from '@ember/debug'; - -import { DEBUG } from '@ember-data/env'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import { assertPolymorphicType } from '../../debug/assert-polymorphic-type'; -import type ManyRelationship from '../../relationships/state/has-many'; -import type { ReplaceRelatedRecordsOperation } from '../-operations'; -import { isBelongsTo, isHasMany, isNew, notifyChange } from '../-utils'; -import type { Graph } from '../graph'; - -/* - case many:1 - ======== - In a bi-directional graph with Many:1 edges, adding a value - results in up-to 3 discrete value transitions, while removing - a value is only 2 transitions. - - For adding C to A - If: A <<-> B, C <->> D is the initial state, - and: B <->> A <<-> C, D is the final state - - then we would undergo the following transitions. - - add C to A - remove C from D - add A to C - - For removing B from A - If: A <<-> B, C <->> D is the initial state, - and: A, B, C <->> D is the final state - - then we would undergo the following transitions. - - remove B from A - remove A from B - - case many:many - =========== - In a bi-directional graph with Many:Many edges, adding or - removing a value requires only 2 value transitions. - - For Adding - If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) - And: D<<->>C<<->>A<<->>B is the final state - - Then we would undergo two transitions. - - add C to A. - add A to C - - For Removing - If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) - And: A, B, C<<->>D is the final state - - Then we would undergo two transitions. - - remove B from A - remove A from B - - case many:? - ======== - In a uni-directional graph with Many:? edges (modeled in EmberData with `inverse:null`) with - artificial (implicit) inverses, replacing a value results in 2 discrete value transitions. - This is because a Many:? relationship is effectively Many:Many. - */ -export default function replaceRelatedRecords(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { - if (isRemote) { - replaceRelatedRecordsRemote(graph, op, isRemote); - } else { - replaceRelatedRecordsLocal(graph, op, isRemote); - } -} - -function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { - const identifiers = op.value; - const relationship = graph.get(op.record, op.field); - assert(`expected hasMany relationship`, isHasMany(relationship)); - relationship.state.hasReceivedData = true; - - // cache existing state - const { localState, localMembers, definition } = relationship; - const newValues = new Set(identifiers); - const identifiersLength = identifiers.length; - const newState = new Array(newValues.size); - const newMembership = new Set(); - - // wipe existing state - relationship.localMembers = newMembership; - relationship.localState = newState; - - const { type } = relationship.definition; - - let changed = false; - - const currentLength = localState.length; - const iterationLength = currentLength > identifiersLength ? currentLength : identifiersLength; - const equalLength = currentLength === identifiersLength; - - for (let i = 0, j = 0; i < iterationLength; i++) { - let adv = false; - if (i < identifiersLength) { - const identifier = identifiers[i]; - // skip processing if we encounter a duplicate identifier in the array - if (!newMembership.has(identifier)) { - if (type !== identifier.type) { - if (DEBUG) { - assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); - } - graph.registerPolymorphicType(type, identifier.type); - } - newState[j] = identifier; - adv = true; - newMembership.add(identifier); - - if (!localMembers.has(identifier)) { - changed = true; - addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); - } - } - } - if (i < currentLength) { - const identifier = localState[i]; - - // detect reordering - if (!newMembership.has(identifier)) { - if (equalLength && newState[i] !== identifier) { - changed = true; - } - - if (!newValues.has(identifier)) { - changed = true; - removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); - } - } - } - if (adv) { - j++; - } - } - - if (changed) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } -} - -function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { - const identifiers = op.value; - const relationship = graph.get(op.record, op.field); - - assert( - `You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, - isHasMany(relationship) - ); - if (isRemote) { - graph._addToTransaction(relationship); - } - relationship.state.hasReceivedData = true; - - // cache existing state - const { remoteState, remoteMembers, definition } = relationship; - const newValues = new Set(identifiers); - const identifiersLength = identifiers.length; - const newState = new Array(newValues.size); - const newMembership = new Set(); - - // wipe existing state - relationship.remoteMembers = newMembership; - relationship.remoteState = newState; - - const { type } = relationship.definition; - - let changed = false; - - const canonicalLength = remoteState.length; - const iterationLength = canonicalLength > identifiersLength ? canonicalLength : identifiersLength; - const equalLength = canonicalLength === identifiersLength; - - for (let i = 0, j = 0; i < iterationLength; i++) { - let adv = false; - if (i < identifiersLength) { - const identifier = identifiers[i]; - if (!newMembership.has(identifier)) { - if (type !== identifier.type) { - if (DEBUG) { - assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); - } - graph.registerPolymorphicType(type, identifier.type); - } - newState[j] = identifier; - newMembership.add(identifier); - adv = true; - - if (!remoteMembers.has(identifier)) { - changed = true; - addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); - } - } - } - if (i < canonicalLength) { - const identifier = remoteState[i]; - - if (!newMembership.has(identifier)) { - // detect reordering - if (equalLength && newState[j] !== identifier) { - changed = true; - } - - if (!newValues.has(identifier)) { - changed = true; - removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); - } - } - } - if (adv) { - j++; - } - } - - if (changed) { - flushCanonical(graph, relationship); - /* - replaceRelatedRecordsLocal( - graph, - { - op: op.op, - record: op.record, - field: op.field, - value: remoteState, - }, - false - );*/ - } else { - // preserve legacy behavior we want to change but requires some sort - // of deprecation. - flushCanonical(graph, relationship); - } -} - -export function addToInverse( - graph: Graph, - identifier: StableRecordIdentifier, - key: string, - value: StableRecordIdentifier, - isRemote: boolean -) { - const relationship = graph.get(identifier, key); - const { type } = relationship.definition; - - if (type !== value.type) { - if (DEBUG) { - assertPolymorphicType(relationship.identifier, relationship.definition, value, graph.store); - } - graph.registerPolymorphicType(type, value.type); - } - - if (isBelongsTo(relationship)) { - relationship.state.hasReceivedData = true; - relationship.state.isEmpty = false; - - if (isRemote) { - graph._addToTransaction(relationship); - if (relationship.remoteState !== null) { - removeFromInverse(graph, relationship.remoteState, relationship.definition.inverseKey, identifier, isRemote); - } - relationship.remoteState = value; - } - - if (relationship.localState !== value) { - if (!isRemote && relationship.localState) { - removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, identifier, isRemote); - } - relationship.localState = value; - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } else if (isHasMany(relationship)) { - if (isRemote) { - if (!relationship.remoteMembers.has(value)) { - graph._addToTransaction(relationship); - relationship.remoteState.push(value); - relationship.remoteMembers.add(value); - relationship.state.hasReceivedData = true; - flushCanonical(graph, relationship); - } - } else { - if (!relationship.localMembers.has(value)) { - relationship.localState.push(value); - relationship.localMembers.add(value); - relationship.state.hasReceivedData = true; - notifyChange(graph, relationship.identifier, relationship.definition.key); - } - } - } else { - if (isRemote) { - if (!relationship.remoteMembers.has(value)) { - relationship.remoteMembers.add(value); - relationship.localMembers.add(value); - } - } else { - if (!relationship.localMembers.has(value)) { - relationship.localMembers.add(value); - } - } - } -} - -export function notifyInverseOfPotentialMaterialization( - graph: Graph, - identifier: StableRecordIdentifier, - key: string, - value: StableRecordIdentifier, - isRemote: boolean -) { - const relationship = graph.get(identifier, key); - if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) { - notifyChange(graph, relationship.identifier, relationship.definition.key); - } -} - -export function removeFromInverse( - graph: Graph, - identifier: StableRecordIdentifier, - key: string, - value: StableRecordIdentifier, - isRemote: boolean -) { - const relationship = graph.get(identifier, key); - - if (isBelongsTo(relationship)) { - relationship.state.isEmpty = true; - if (isRemote) { - graph._addToTransaction(relationship); - relationship.remoteState = null; - } - if (relationship.localState === value) { - relationship.localState = null; - - notifyChange(graph, identifier, key); - } - } else if (isHasMany(relationship)) { - if (isRemote) { - graph._addToTransaction(relationship); - let index = relationship.remoteState.indexOf(value); - if (index !== -1) { - relationship.remoteMembers.delete(value); - relationship.remoteState.splice(index, 1); - } - } - let index = relationship.localState.indexOf(value); - if (index !== -1) { - relationship.localMembers.delete(value); - relationship.localState.splice(index, 1); - } - notifyChange(graph, relationship.identifier, relationship.definition.key); - } else { - if (isRemote) { - relationship.remoteMembers.delete(value); - relationship.localMembers.delete(value); - } else { - if (value && relationship.localMembers.has(value)) { - relationship.localMembers.delete(value); - } - } - } -} - -export function syncRemoteToLocal(graph: Graph, rel: ManyRelationship) { - let toSet = rel.remoteState; - let newIdentifiers = rel.localState.filter((identifier) => isNew(identifier) && toSet.indexOf(identifier) === -1); - let existingState = rel.localState; - rel.localState = toSet.concat(newIdentifiers); - - let localMembers = (rel.localMembers = new Set()); - rel.remoteMembers.forEach((v) => localMembers.add(v)); - for (let i = 0; i < newIdentifiers.length; i++) { - localMembers.add(newIdentifiers[i]); - } - - // TODO always notifying fails only one test and we should probably do away with it - if (existingState.length !== rel.localState.length) { - notifyChange(graph, rel.identifier, rel.definition.key); - } else { - for (let i = 0; i < existingState.length; i++) { - if (existingState[i] !== rel.localState[i]) { - notifyChange(graph, rel.identifier, rel.definition.key); - break; - } - } - } -} - -function flushCanonical(graph: Graph, rel: ManyRelationship) { - graph._scheduleLocalSync(rel); -} diff --git a/packages/graph/src/-private/graph/operations/update-relationship.ts b/packages/graph/src/-private/graph/operations/update-relationship.ts deleted file mode 100644 index 6018198ab9e..00000000000 --- a/packages/graph/src/-private/graph/operations/update-relationship.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { assert, warn } from '@ember/debug'; - -import type { ExistingResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; - -import _normalizeLink from '../../normalize-link'; -import type { UpdateRelationshipOperation } from '../-operations'; -import { isBelongsTo, isHasMany, notifyChange } from '../-utils'; -import type { Graph } from '../graph'; - -/* - Updates the "canonical" or "remote" state of a relationship, replacing any existing - state and blowing away any local changes (excepting new records). -*/ -export default function updateRelationshipOperation(graph: Graph, op: UpdateRelationshipOperation) { - const relationship = graph.get(op.record, op.field); - assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); - const { definition, state, identifier } = relationship; - const { isCollection } = definition; - - const payload = op.value; - - let hasRelationshipDataProperty: boolean = false; - let hasUpdatedLink: boolean = false; - - if (payload.meta) { - relationship.meta = payload.meta; - } - - if (payload.data !== undefined) { - hasRelationshipDataProperty = true; - if (isCollection) { - // TODO deprecate this case. We - // have tests saying we support it. - if (payload.data === null) { - payload.data = []; - } - assert(`Expected an array`, Array.isArray(payload.data)); - const cache = graph.store.identifierCache; - // TODO may not need to cast to stable identifiers here since update likely does this too - graph.update( - { - op: 'replaceRelatedRecords', - record: identifier, - field: op.field, - value: payload.data.map((i) => cache.getOrCreateRecordIdentifier(i)), - }, - true - ); - } else { - // TODO may not need to cast to stable identifiers here since update likely does this too - graph.update( - { - op: 'replaceRelatedRecord', - record: identifier, - field: op.field, - value: payload.data - ? graph.store.identifierCache.getOrCreateRecordIdentifier(payload.data as ExistingResourceIdentifierObject) - : null, - }, - true - ); - } - } else if (definition.isAsync === false && !state.hasReceivedData) { - hasRelationshipDataProperty = true; - - if (isCollection) { - graph.update( - { - op: 'replaceRelatedRecords', - record: identifier, - field: op.field, - value: [], - }, - true - ); - } else { - graph.update( - { - op: 'replaceRelatedRecord', - record: identifier, - field: op.field, - value: null, - }, - true - ); - } - } - - if (payload.links) { - let originalLinks = relationship.links; - relationship.links = payload.links; - if (payload.links.related) { - let relatedLink = _normalizeLink(payload.links.related); - let currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null; - let currentLinkHref = currentLink ? currentLink.href : null; - - if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) { - warn( - `You pushed a record of type '${identifier.type}' with a relationship '${definition.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, - definition.isAsync || state.hasReceivedData, - { - id: 'ds.store.push-link-for-sync-relationship', - } - ); - assert( - `You have pushed a record of type '${identifier.type}' with '${definition.key}' as a link, but the value of that link is not a string.`, - typeof relatedLink.href === 'string' || relatedLink.href === null - ); - hasUpdatedLink = true; - } - } - } - - /* - Data being pushed into the relationship might contain only data or links, - or a combination of both. - - IF contains only data - IF contains both links and data - state.isEmpty -> true if is empty array (has-many) or is null (belongs-to) - state.hasReceivedData -> true - hasDematerializedInverse -> false - state.isStale -> false - allInverseRecordsAreLoaded -> run-check-to-determine - - IF contains only links - state.isStale -> true - */ - relationship.state.hasFailedLoadAttempt = false; - if (hasRelationshipDataProperty) { - let relationshipIsEmpty = payload.data === null || (Array.isArray(payload.data) && payload.data.length === 0); - - // we don't need to notify here as the update op we pushed in above will notify once - // membership is in the correct state. - relationship.state.hasReceivedData = true; - relationship.state.isStale = false; - relationship.state.hasDematerializedInverse = false; - relationship.state.isEmpty = relationshipIsEmpty; - } else if (hasUpdatedLink) { - // only notify stale if we have not previously received membership data. - // within this same transaction - // this prevents refetching when only one side of the relationship in the - // payload contains the info while the other side contains just a link - // this only works when the side with just a link is a belongsTo, as we - // don't know if a hasMany has full information or not. - // see #7049 for context. - if (isCollection || !relationship.state.hasReceivedData || relationship.transactionRef === 0) { - relationship.state.isStale = true; - - notifyChange(graph, relationship.identifier, relationship.definition.key); - } else { - relationship.state.isStale = false; - } - } -} diff --git a/packages/graph/src/-private/normalize-link.ts b/packages/graph/src/-private/normalize-link.ts index ea18acd0e8a..a7214b5c798 100644 --- a/packages/graph/src/-private/normalize-link.ts +++ b/packages/graph/src/-private/normalize-link.ts @@ -1,4 +1,4 @@ -import type { Link, LinkObject } from '@ember-data/types/q/ember-data-json-api'; +import type { Link, LinkObject } from '@warp-drive/core-types/spec/json-api-raw'; /* This method normalizes a link to an "links object". If the passed link is diff --git a/packages/graph/src/-private/operations/add-to-related-records.ts b/packages/graph/src/-private/operations/add-to-related-records.ts new file mode 100644 index 00000000000..2c08cb4981b --- /dev/null +++ b/packages/graph/src/-private/operations/add-to-related-records.ts @@ -0,0 +1,46 @@ +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { AddToRelatedRecordsOperation } from '@warp-drive/core-types/graph'; + +import { _addLocal } from '../-diff'; +import { isHasMany, notifyChange } from '../-utils'; +import type { CollectionEdge } from '../edges/collection'; +import type { Graph } from '../graph'; +import { addToInverse } from './replace-related-records'; + +export default function addToRelatedRecords(graph: Graph, op: AddToRelatedRecordsOperation, isRemote: false) { + assert( + `Graph does not yet support updating the remote state of a relationship via the ${op.op} operation`, + !isRemote + ); + const { record, value, index } = op; + const relationship = graph.get(record, op.field); + assert( + `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, + isHasMany(relationship) + ); + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + addRelatedRecord(graph, relationship, record, value[i], index !== undefined ? index + i : index, isRemote); + } + } else { + addRelatedRecord(graph, relationship, record, value, index, isRemote); + } + + notifyChange(graph, relationship.identifier, relationship.definition.key); +} + +function addRelatedRecord( + graph: Graph, + relationship: CollectionEdge, + record: StableRecordIdentifier, + value: StableRecordIdentifier, + index: number | undefined, + isRemote: false +) { + assert(`expected an identifier to add to the collection relationship`, value); + + if (_addLocal(graph, record, relationship, value, index ?? null)) { + addToInverse(graph, value, relationship.definition.inverseKey, record, isRemote); + } +} diff --git a/packages/graph/src/-private/operations/merge-identifier.ts b/packages/graph/src/-private/operations/merge-identifier.ts new file mode 100644 index 00000000000..8c95f40179b --- /dev/null +++ b/packages/graph/src/-private/operations/merge-identifier.ts @@ -0,0 +1,79 @@ +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; + +import { forAllRelatedIdentifiers, isBelongsTo, isHasMany, notifyChange } from '../-utils'; +import type { CollectionEdge } from '../edges/collection'; +import type { ImplicitEdge } from '../edges/implicit'; +import type { ResourceEdge } from '../edges/resource'; +import type { Graph, GraphEdge } from '../graph'; + +export function mergeIdentifier(graph: Graph, op: MergeOperation, relationships: Record) { + Object.keys(relationships).forEach((key) => { + const rel = relationships[key]; + if (!rel) { + return; + } + mergeIdentifierForRelationship(graph, op, rel); + }); +} + +function mergeIdentifierForRelationship(graph: Graph, op: MergeOperation, rel: GraphEdge): void { + rel.identifier = op.value; + forAllRelatedIdentifiers(rel, (identifier) => { + const inverse = graph.get(identifier, rel.definition.inverseKey); + mergeInRelationship(graph, inverse, op); + }); +} + +function mergeInRelationship(graph: Graph, rel: GraphEdge, op: MergeOperation): void { + if (isBelongsTo(rel)) { + mergeBelongsTo(graph, rel, op); + } else if (isHasMany(rel)) { + mergeHasMany(graph, rel, op); + } else { + mergeImplicit(graph, rel, op); + } +} + +function mergeBelongsTo(graph: Graph, rel: ResourceEdge, op: MergeOperation): void { + if (rel.remoteState === op.record) { + rel.remoteState = op.value; + } + if (rel.localState === op.record) { + rel.localState = op.value; + notifyChange(graph, rel.identifier, rel.definition.key); + } +} + +function mergeHasMany(graph: Graph, rel: CollectionEdge, op: MergeOperation): void { + if (rel.remoteMembers.has(op.record)) { + rel.remoteMembers.delete(op.record); + rel.remoteMembers.add(op.value); + const index = rel.remoteState.indexOf(op.record); + rel.remoteState.splice(index, 1, op.value); + rel.isDirty = true; + } + if (rel.additions?.has(op.record)) { + rel.additions.delete(op.record); + rel.additions.add(op.value); + rel.isDirty = true; + } + if (rel.removals?.has(op.record)) { + rel.removals.delete(op.record); + rel.removals.add(op.value); + rel.isDirty = true; + } + if (rel.isDirty) { + notifyChange(graph, rel.identifier, rel.definition.key); + } +} + +function mergeImplicit(graph: Graph, rel: ImplicitEdge, op: MergeOperation): void { + if (rel.remoteMembers.has(op.record)) { + rel.remoteMembers.delete(op.record); + rel.remoteMembers.add(op.value); + } + if (rel.localMembers.has(op.record)) { + rel.localMembers.delete(op.record); + rel.localMembers.add(op.value); + } +} diff --git a/packages/graph/src/-private/operations/remove-from-related-records.ts b/packages/graph/src/-private/operations/remove-from-related-records.ts new file mode 100644 index 00000000000..63f198d136f --- /dev/null +++ b/packages/graph/src/-private/operations/remove-from-related-records.ts @@ -0,0 +1,47 @@ +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { RemoveFromRelatedRecordsOperation } from '@warp-drive/core-types/graph'; + +import { _removeLocal } from '../-diff'; +import { isHasMany, notifyChange } from '../-utils'; +import type { CollectionEdge } from '../edges/collection'; +import type { Graph } from '../graph'; +import { removeFromInverse } from './replace-related-records'; + +export default function removeFromRelatedRecords(graph: Graph, op: RemoveFromRelatedRecordsOperation, isRemote: false) { + assert( + `Graph does not yet support updating the remote state of a relationship via the ${op.op} operation`, + !isRemote + ); + const { record, value } = op; + const relationship = graph.get(record, op.field); + assert( + `You can only '${op.op}' on a hasMany relationship. ${record.type}.${op.field} is a ${relationship.definition.kind}`, + isHasMany(relationship) + ); + // TODO we should potentially thread the index information through here + // when available as it may make it faster to remove from the local state + // when trying to patch more efficiently without blowing away the entire + // local state array + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + removeRelatedRecord(graph, relationship, record, value[i], isRemote); + } + } else { + removeRelatedRecord(graph, relationship, record, value, isRemote); + } + notifyChange(graph, relationship.identifier, relationship.definition.key); +} + +function removeRelatedRecord( + graph: Graph, + relationship: CollectionEdge, + record: StableRecordIdentifier, + value: StableRecordIdentifier, + isRemote: false +) { + assert(`expected an identifier to remove from the collection relationship`, value); + if (_removeLocal(relationship, value)) { + removeFromInverse(graph, value, relationship.definition.inverseKey, record, isRemote); + } +} diff --git a/packages/graph/src/-private/operations/replace-related-record.ts b/packages/graph/src/-private/operations/replace-related-record.ts new file mode 100644 index 00000000000..ee8833f2cbb --- /dev/null +++ b/packages/graph/src/-private/operations/replace-related-record.ts @@ -0,0 +1,198 @@ +import { deprecate } from '@ember/debug'; + +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'; +import type { ReplaceRelatedRecordOperation } from '@warp-drive/core-types/graph'; + +import { isBelongsTo, isNew, notifyChange } from '../-utils'; +import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; +import type { Graph } from '../graph'; +import { addToInverse, notifyInverseOfPotentialMaterialization, removeFromInverse } from './replace-related-records'; + +export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRecordOperation, isRemote = false) { + const relationship = graph.get(op.record, op.field); + assert( + `You can only '${op.op}' on a belongsTo relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, + isBelongsTo(relationship) + ); + if (isRemote) { + graph._addToTransaction(relationship); + } + const { definition, state } = relationship; + const prop = isRemote ? 'remoteState' : 'localState'; + const existingState: StableRecordIdentifier | null = relationship[prop]; + + /* + case 1:1 + ======== + In a bi-directional graph with 1:1 edges, replacing a value + results in up-to 4 discrete value transitions. + + If: A <-> B, C <-> D is the initial state, + and: A <-> C, B, D is the final state + + then we would undergo the following 4 transitions. + + remove A from B + add C to A + remove C from D + add A to C + + case 1:many + =========== + In a bi-directional graph with 1:Many edges, replacing a value + results in up-to 3 discrete value transitions. + + If: A<->>B<<->D, C<<->D is the initial state (double arrows representing the many side) + And: A<->>C<<->D, B<<->D is the final state + + Then we would undergo three transitions. + + remove A from B + add C to A. + add A to C + + case 1:? + ======== + In a uni-directional graph with 1:? edges (modeled in EmberData with `inverse:null`) with + artificial (implicit) inverses, replacing a value results in up-to 3 discrete value transitions. + This is because a 1:? relationship is effectively 1:many. + + If: A->B, C->B is the initial state + And: A->C, C->B is the final state + + Then we would undergo three transitions. + + Remove A from B + Add C to A + Add A to C + */ + + // nothing for us to do + if (op.value === existingState) { + // if we were empty before but now know we are empty this needs to be true + state.hasReceivedData = true; + // if this is a remote update we still sync + if (isRemote) { + const { localState } = relationship; + // don't sync if localState is a new record and our remoteState is null + if (localState && isNew(localState) && !existingState) { + return; + } + if (existingState && localState === existingState) { + notifyInverseOfPotentialMaterialization(graph, existingState, definition.inverseKey, op.record, isRemote); + } else if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + // if localState does not match existingState then we know + // we have a local mutation that has not been persisted yet + if (localState !== op.value && relationship.definition.resetOnRemoteUpdate !== false) { + relationship.localState = existingState; + + deprecate( + `EmberData is changing the default semantics of updates to the remote state of relationships.\n\nThe following local state was cleared from the <${ + relationship.identifier.type + }>.${ + relationship.definition.key + } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ + localState ? 'Added: ' + localState.lid + '\n\t' : '' + }${existingState ? 'Removed: ' + existingState.lid : ''}`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + for: 'ember-data', + since: { enabled: '5.3', available: '4.13' }, + until: '6.0', + url: 'https://deprecations.emberjs.com/v5.x#ember-data-deprecate-relationship-remote-update-clearing-local-state', + } + ); + + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } + } + return; + } + + // remove this value from the inverse if required + if (existingState) { + removeFromInverse(graph, existingState, definition.inverseKey, op.record, isRemote); + } + + // update value to the new value + relationship[prop] = op.value; + state.hasReceivedData = true; + state.isEmpty = op.value === null; + state.isStale = false; + state.hasFailedLoadAttempt = false; + + if (op.value) { + if (definition.type !== op.value.type) { + // assert( + // `The '<${definition.inverseType}>.${op.field}' relationship expects only '${definition.type}' records since it is not polymorphic. Received a Record of type '${op.value.type}'`, + // definition.isPolymorphic + // ); + + // TODO this should now handle the deprecation warning if isPolymorphic is not set + // but the record does turn out to be polymorphic + // this should still assert if the user is relying on legacy inheritance/mixins to + // provide polymorphic behavior and has not yet added the polymorphic flags + if (DEBUG) { + assertPolymorphicType(relationship.identifier, definition, op.value, graph.store); + } + + graph.registerPolymorphicType(definition.type, op.value.type); + } + addToInverse(graph, op.value, definition.inverseKey, op.record, isRemote); + } + + if (isRemote) { + const { localState, remoteState } = relationship; + if (localState && isNew(localState) && !remoteState) { + return; + } + // when localState does not match the new remoteState and + // localState === existingState then we had no local mutation + // and we can safely sync the new remoteState to local + if (localState !== remoteState && localState === existingState) { + relationship.localState = remoteState; + 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. + } else if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + if ( + localState !== remoteState && + localState !== existingState && + relationship.definition.resetOnRemoteUpdate !== false + ) { + relationship.localState = remoteState; + + deprecate( + `EmberData is changing the default semantics of updates to the remote state of relationships.\n\nThe following local state was cleared from the <${ + relationship.identifier.type + }>.${ + relationship.definition.key + } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ + localState ? 'Added: ' + localState.lid + '\n\t' : '' + }${existingState ? 'Removed: ' + existingState.lid : ''}`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + for: 'ember-data', + since: { enabled: '5.3', available: '4.13' }, + until: '6.0', + url: 'https://deprecations.emberjs.com/v5.x#ember-data-deprecate-relationship-remote-update-clearing-local-state', + } + ); + + notifyChange(graph, relationship.identifier, relationship.definition.key); + } + } + } else { + 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 new file mode 100644 index 00000000000..6cc3c58c6a7 --- /dev/null +++ b/packages/graph/src/-private/operations/replace-related-records.ts @@ -0,0 +1,443 @@ +import { deprecate } from '@ember/debug'; + +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'; +import type { ReplaceRelatedRecordsOperation } from '@warp-drive/core-types/graph'; + +import { _addLocal, _removeLocal, _removeRemote, diffCollection } from '../-diff'; +import { isBelongsTo, isHasMany, isNew, notifyChange } from '../-utils'; +import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; +import type { CollectionEdge } from '../edges/collection'; +import type { Graph } from '../graph'; + +/* + case many:1 + ======== + In a bi-directional graph with Many:1 edges, adding a value + results in up-to 3 discrete value transitions, while removing + a value is only 2 transitions. + + For adding C to A + If: A <<-> B, C <->> D is the initial state, + and: B <->> A <<-> C, D is the final state + + then we would undergo the following transitions. + + add C to A + remove C from D + add A to C + + For removing B from A + If: A <<-> B, C <->> D is the initial state, + and: A, B, C <->> D is the final state + + then we would undergo the following transitions. + + remove B from A + remove A from B + + case many:many + =========== + In a bi-directional graph with Many:Many edges, adding or + removing a value requires only 2 value transitions. + + For Adding + If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) + And: D<<->>C<<->>A<<->>B is the final state + + Then we would undergo two transitions. + + add C to A. + add A to C + + For Removing + If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) + And: A, B, C<<->>D is the final state + + Then we would undergo two transitions. + + remove B from A + remove A from B + + case many:? + ======== + In a uni-directional graph with Many:? edges (modeled in EmberData with `inverse:null`) with + artificial (implicit) inverses, replacing a value results in 2 discrete value transitions. + This is because a Many:? relationship is effectively Many:Many. + */ +export default function replaceRelatedRecords(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { + if (isRemote) { + replaceRelatedRecordsRemote(graph, op, isRemote); + } else { + replaceRelatedRecordsLocal(graph, op, isRemote); + } +} + +function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { + const identifiers = op.value; + 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; + relationship.isDirty = false; + + const onAdd = (identifier: StableRecordIdentifier) => { + // Since we are diffing against the remote state, we check + // if our previous local state did not contain this identifier + const removalsHas = removals?.has(identifier); + if (removalsHas || !additions?.has(identifier)) { + if (type !== identifier.type) { + if (DEBUG) { + assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); + } + graph.registerPolymorphicType(type, identifier.type); + } + + relationship.isDirty = true; + addToInverse(graph, identifier, inverseKey, op.record, isRemote); + + if (removalsHas) { + removals!.delete(identifier); + } + } + }; + + const onRemove = (identifier: StableRecordIdentifier) => { + // Since we are diffing against the remote state, we check + // if our previous local state had contained this identifier + const additionsHas = additions?.has(identifier); + if (additionsHas || !removals?.has(identifier)) { + relationship.isDirty = true; + removeFromInverse(graph, identifier, inverseKey, record, isRemote); + + if (additionsHas) { + additions!.delete(identifier); + } + } + }; + + 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 + // need to be removed from the inverse + if (additions && additions.size > 0) { + additions.forEach((identifier) => { + if (!diff.add.has(identifier)) { + becameDirty = true; + onRemove(identifier); + } + }); + } + + // any removals no longer in the local state + // need to be added back to the inverse + if (removals && removals.size > 0) { + removals.forEach((identifier) => { + if (!diff.del.has(identifier)) { + becameDirty = true; + onAdd(identifier); + } + }); + } + + relationship.additions = diff.add; + relationship.removals = diff.del; + relationship.localState = diff.finalState; + relationship.isDirty = wasDirty; + + if ( + isMaybeFirstUpdate || + !wasDirty /*&& becameDirty // TODO to guard like this we need to detect reorder when diffing local */ + ) { + notifyChange(graph, op.record, op.field); + } +} + +function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { + const identifiers = op.value; + const relationship = graph.get(op.record, op.field); + + assert( + `You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, + isHasMany(relationship) + ); + if (isRemote) { + graph._addToTransaction(relationship); + } + relationship.state.hasReceivedData = true; + + // cache existing state + const { definition } = relationship; + + const { type } = relationship.definition; + + const diff = diffCollection( + identifiers, + relationship, + (identifier) => { + if (type !== identifier.type) { + if (DEBUG) { + assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); + } + graph.registerPolymorphicType(type, identifier.type); + } + // commit additions + // TODO build this into the diff? + // because we are not dirty if this was a committed local addition + if (relationship.additions?.has(identifier)) { + relationship.additions.delete(identifier); + } else { + relationship.isDirty = true; + } + addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); + }, + (identifier) => { + // commit removals + // TODO build this into the diff? + // because we are not dirty if this was a committed local addition + if (relationship.removals?.has(identifier)) { + relationship.removals.delete(identifier); + } else { + relationship.isDirty = true; + } + removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); + } + ); + + // replace existing state + relationship.remoteMembers = diff.finalSet; + relationship.remoteState = diff.finalState; + + // changed also indicates a change in order + if (diff.changed) { + relationship.isDirty = true; + } + + // TODO unsure if we need this but it + // may allow us to more efficiently patch + // the associated ManyArray + relationship._diff = diff; + + if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + // only do this for legacy hasMany, not collection + // and provide a way to incrementally migrate + if (relationship.definition.kind === 'hasMany' && relationship.definition.resetOnRemoteUpdate !== false) { + const deprecationInfo: { + removals: StableRecordIdentifier[]; + additions: StableRecordIdentifier[]; + triggered: boolean; + } = { + removals: [], + additions: [], + triggered: false, + }; + if (relationship.removals) { + relationship.isDirty = true; + relationship.removals.forEach((identifier) => { + deprecationInfo.triggered = true; + deprecationInfo.removals.push(identifier); + // reverse the removal + // 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, isRemote); + }); + relationship.removals = null; + } + if (relationship.additions) { + relationship.additions.forEach((identifier) => { + // reverse the addition + // if we are still in additions at this point then + // we were not "committed" which means we are not present + // in the remoteMembers. So we "remove" from the inverse. + // however we only do this if we are not a "new" record. + if (!isNew(identifier)) { + deprecationInfo.triggered = true; + deprecationInfo.additions.push(identifier); + relationship.isDirty = true; + relationship.additions!.delete(identifier); + removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); + } + }); + if (relationship.additions.size === 0) { + relationship.additions = null; + } + } + + if (deprecationInfo.triggered) { + deprecate( + `EmberData is changing the default semantics of updates to the remote state of relationships.\n\nThe following local state was cleared from the <${ + relationship.identifier.type + }>.${ + relationship.definition.key + } 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(', ')}]`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + for: 'ember-data', + since: { enabled: '5.3', available: '4.13' }, + until: '6.0', + url: 'https://deprecations.emberjs.com/v5.x#ember-data-deprecate-relationship-remote-update-clearing-local-state', + } + ); + } + } + } + + if (relationship.isDirty) { + flushCanonical(graph, relationship); + } +} + +export function addToInverse( + graph: Graph, + identifier: StableRecordIdentifier, + key: string, + value: StableRecordIdentifier, + isRemote: boolean +) { + const relationship = graph.get(identifier, key); + const { type } = relationship.definition; + + if (type !== value.type) { + if (DEBUG) { + assertPolymorphicType(relationship.identifier, relationship.definition, value, graph.store); + } + graph.registerPolymorphicType(type, value.type); + } + + if (isBelongsTo(relationship)) { + relationship.state.hasReceivedData = true; + relationship.state.isEmpty = false; + + if (isRemote) { + graph._addToTransaction(relationship); + if (relationship.remoteState !== null) { + removeFromInverse(graph, relationship.remoteState, relationship.definition.inverseKey, identifier, isRemote); + } + relationship.remoteState = value; + } + + if (relationship.localState !== value) { + if (!isRemote && relationship.localState) { + removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, identifier, isRemote); + } + relationship.localState = value; + notifyChange(graph, identifier, key); + } + } else if (isHasMany(relationship)) { + if (isRemote) { + // TODO this needs to alert stuffs + // And patch state better + // This is almost definitely wrong + // WARNING WARNING WARNING + + if (!relationship.remoteMembers.has(value)) { + graph._addToTransaction(relationship); + relationship.remoteState.push(value); + relationship.remoteMembers.add(value); + if (relationship.additions?.has(value)) { + relationship.additions.delete(value); + } else { + relationship.isDirty = true; + relationship.state.hasReceivedData = true; + flushCanonical(graph, relationship); + } + } + } else { + if (_addLocal(graph, identifier, relationship, value, null)) { + notifyChange(graph, identifier, key); + } + } + } else { + if (isRemote) { + if (!relationship.remoteMembers.has(value)) { + relationship.remoteMembers.add(value); + relationship.localMembers.add(value); + } + } else { + if (!relationship.localMembers.has(value)) { + relationship.localMembers.add(value); + } + } + } +} + +export function notifyInverseOfPotentialMaterialization( + graph: Graph, + identifier: StableRecordIdentifier, + key: string, + value: StableRecordIdentifier, + isRemote: boolean +) { + const relationship = graph.get(identifier, key); + if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) { + notifyChange(graph, identifier, key); + } +} + +export function removeFromInverse( + graph: Graph, + identifier: StableRecordIdentifier, + key: string, + value: StableRecordIdentifier, + isRemote: boolean +) { + const relationship = graph.get(identifier, key); + + if (isBelongsTo(relationship)) { + relationship.state.isEmpty = true; + if (isRemote) { + graph._addToTransaction(relationship); + relationship.remoteState = null; + } + if (relationship.localState === value) { + relationship.localState = null; + + notifyChange(graph, identifier, key); + } + } else if (isHasMany(relationship)) { + if (isRemote) { + graph._addToTransaction(relationship); + if (_removeRemote(relationship, value)) { + notifyChange(graph, identifier, key); + } + } else { + if (_removeLocal(relationship, value)) { + notifyChange(graph, identifier, key); + } + } + } else { + if (isRemote) { + relationship.remoteMembers.delete(value); + relationship.localMembers.delete(value); + } else { + if (value && relationship.localMembers.has(value)) { + relationship.localMembers.delete(value); + } + } + } +} + +function flushCanonical(graph: Graph, rel: CollectionEdge) { + graph._scheduleLocalSync(rel); +} diff --git a/packages/graph/src/-private/operations/update-relationship.ts b/packages/graph/src/-private/operations/update-relationship.ts new file mode 100644 index 00000000000..96fd82f7887 --- /dev/null +++ b/packages/graph/src/-private/operations/update-relationship.ts @@ -0,0 +1,183 @@ +import { warn } from '@ember/debug'; + +import type Store from '@ember-data/store'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { UpdateRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { + ExistingResourceIdentifierObject, + NewResourceIdentifierObject, +} from '@warp-drive/core-types/spec/json-api-raw'; + +import { isBelongsTo, isHasMany, notifyChange } from '../-utils'; +import type { Graph } from '../graph'; +import _normalizeLink from '../normalize-link'; + +type IdentifierCache = Store['identifierCache']; + +/* + Updates the "canonical" or "remote" state of a relationship, replacing any existing + state and blowing away any local changes (excepting new records). +*/ +export default function updateRelationshipOperation(graph: Graph, op: UpdateRelationshipOperation) { + const relationship = graph.get(op.record, op.field); + assert(`Cannot update an implicit relationship`, isHasMany(relationship) || isBelongsTo(relationship)); + const { definition, state, identifier } = relationship; + const { isCollection } = definition; + + const payload = op.value; + + let hasRelationshipDataProperty = false; + let hasUpdatedLink = false; + + if (payload.meta) { + relationship.meta = payload.meta; + } + + if (payload.data !== undefined) { + hasRelationshipDataProperty = true; + if (isCollection) { + // TODO deprecate this case. We + // have tests saying we support it. + if (payload.data === null) { + payload.data = []; + } + assert(`Expected an array`, Array.isArray(payload.data)); + const cache = graph.store.identifierCache; + graph.update( + { + op: 'replaceRelatedRecords', + record: identifier, + field: op.field, + value: upgradeIdentifiers(payload.data, cache), + }, + true + ); + } else { + graph.update( + { + op: 'replaceRelatedRecord', + record: identifier, + field: op.field, + value: payload.data + ? graph.store.identifierCache.upgradeIdentifier(payload.data as ExistingResourceIdentifierObject) + : null, + }, + true + ); + } + } else if (definition.isAsync === false && !state.hasReceivedData) { + hasRelationshipDataProperty = true; + + if (isCollection) { + graph.update( + { + op: 'replaceRelatedRecords', + record: identifier, + field: op.field, + value: [], + }, + true + ); + } else { + graph.update( + { + op: 'replaceRelatedRecord', + record: identifier, + field: op.field, + value: null, + }, + true + ); + } + } + + if (payload.links) { + const originalLinks = relationship.links; + relationship.links = payload.links; + if (payload.links.related) { + const relatedLink = _normalizeLink(payload.links.related); + const currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null; + const currentLinkHref = currentLink ? currentLink.href : null; + + if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) { + warn( + `You pushed a record of type '${identifier.type}' with a relationship '${definition.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, + definition.isAsync || state.hasReceivedData, + { + id: 'ds.store.push-link-for-sync-relationship', + } + ); + assert( + `You have pushed a record of type '${identifier.type}' with '${definition.key}' as a link, but the value of that link is not a string.`, + typeof relatedLink.href === 'string' || relatedLink.href === null + ); + hasUpdatedLink = true; + } + } + } + + /* + Data being pushed into the relationship might contain only data or links, + or a combination of both. + + IF contains only data + IF contains both links and data + state.isEmpty -> true if is empty array (has-many) or is null (belongs-to) + state.hasReceivedData -> true + hasDematerializedInverse -> false + state.isStale -> false + allInverseRecordsAreLoaded -> run-check-to-determine + + IF contains only links + state.isStale -> true + */ + relationship.state.hasFailedLoadAttempt = false; + if (hasRelationshipDataProperty) { + const relationshipIsEmpty = payload.data === null || (Array.isArray(payload.data) && payload.data.length === 0); + + // we don't need to notify here as the update op we pushed in above will notify once + // membership is in the correct state. + relationship.state.hasReceivedData = true; + relationship.state.isStale = false; + relationship.state.hasDematerializedInverse = false; + relationship.state.isEmpty = relationshipIsEmpty; + } else if (hasUpdatedLink) { + // only notify stale if we have not previously received membership data. + // within this same transaction + // this prevents refetching when only one side of the relationship in the + // payload contains the info while the other side contains just a link + // this only works when the side with just a link is a belongsTo, as we + // don't know if a hasMany has full information or not. + // see #7049 for context. + if ( + isCollection || + !relationship.state.hasReceivedData || + isStaleTransaction(relationship.transactionRef, graph._transaction) + ) { + relationship.state.isStale = true; + + notifyChange(graph, relationship.identifier, relationship.definition.key); + } else { + relationship.state.isStale = false; + } + } +} + +function isStaleTransaction(relationshipTransactionId: number, graphTransactionId: number | null) { + return ( + relationshipTransactionId === 0 || // relationship has never notified + graphTransactionId === null || // we are not in a transaction + relationshipTransactionId < graphTransactionId // we are not part of the current transaction + ); +} + +export function upgradeIdentifiers( + arr: (ExistingResourceIdentifierObject | NewResourceIdentifierObject | StableRecordIdentifier)[], + cache: IdentifierCache +): StableRecordIdentifier[] { + for (let i = 0; i < arr.length; i++) { + arr[i] = cache.upgradeIdentifier(arr[i]); + } + return arr as StableRecordIdentifier[]; +} diff --git a/packages/graph/src/-private/relationships/state/belongs-to.ts b/packages/graph/src/-private/relationships/state/belongs-to.ts deleted file mode 100644 index 0fedeff4e0e..00000000000 --- a/packages/graph/src/-private/relationships/state/belongs-to.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Links, Meta, PaginationLinks, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import type { UpgradedMeta } from '../../graph/-edge-definition'; -import type { RelationshipState } from '../../graph/-state'; -import { createState } from '../../graph/-state'; - -export default class BelongsToRelationship { - declare definition: UpgradedMeta; - declare identifier: StableRecordIdentifier; - declare _state: RelationshipState | null; - declare transactionRef: number; - - declare localState: StableRecordIdentifier | null; - declare remoteState: StableRecordIdentifier | null; - declare meta: Meta | null; - declare links: Links | PaginationLinks | null; - - constructor(definition: UpgradedMeta, identifier: StableRecordIdentifier) { - this.definition = definition; - this.identifier = identifier; - this._state = null; - this.transactionRef = 0; - - this.meta = null; - this.links = null; - - this.localState = null; - this.remoteState = null; - } - - get state(): RelationshipState { - let { _state } = this; - if (!_state) { - _state = this._state = createState(); - } - return _state; - } - - getData(): SingleResourceRelationship { - let data; - let payload: any = {}; - if (this.localState) { - data = this.localState; - } - if (this.localState === null && this.state.hasReceivedData) { - data = null; - } - if (this.links) { - payload.links = this.links; - } - if (data !== undefined) { - payload.data = data; - } - if (this.meta) { - payload.meta = this.meta; - } - - return payload; - } -} diff --git a/packages/graph/src/-private/relationships/state/has-many.ts b/packages/graph/src/-private/relationships/state/has-many.ts deleted file mode 100755 index 603358ffd9b..00000000000 --- a/packages/graph/src/-private/relationships/state/has-many.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { - CollectionResourceRelationship, - Links, - Meta, - PaginationLinks, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -import type { UpgradedMeta } from '../../graph/-edge-definition'; -import type { RelationshipState } from '../../graph/-state'; -import { createState } from '../../graph/-state'; - -export default class ManyRelationship { - declare definition: UpgradedMeta; - declare identifier: StableRecordIdentifier; - declare _state: RelationshipState | null; - declare transactionRef: number; - - declare localMembers: Set; - declare remoteMembers: Set; - declare meta: Meta | null; - declare links: Links | PaginationLinks | null; - - declare remoteState: StableRecordIdentifier[]; - declare localState: StableRecordIdentifier[]; - - constructor(definition: UpgradedMeta, identifier: StableRecordIdentifier) { - this.definition = definition; - this.identifier = identifier; - this._state = null; - this.transactionRef = 0; - - this.localMembers = new Set(); - this.remoteMembers = new Set(); - - this.meta = null; - this.links = null; - - // persisted state - this.remoteState = []; - // local client state - this.localState = []; - } - - get state(): RelationshipState { - let { _state } = this; - if (!_state) { - _state = this._state = createState(); - } - return _state; - } - - getData(): CollectionResourceRelationship { - let payload: any = {}; - if (this.state.hasReceivedData) { - payload.data = this.localState.slice(); - } - if (this.links) { - payload.links = this.links; - } - if (this.meta) { - payload.meta = this.meta; - } - - return payload; - } -} diff --git a/packages/graph/tsconfig.json b/packages/graph/tsconfig.json new file mode 100644 index 00000000000..843d08492fd --- /dev/null +++ b/packages/graph/tsconfig.json @@ -0,0 +1,67 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "noImplicitAny": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + }, + { + "path": "../request-utils" + } + ] +} diff --git a/packages/graph/vite.config.mjs b/packages/graph/vite.config.mjs new file mode 100644 index 00000000000..a548ae349de --- /dev/null +++ b/packages/graph/vite.config.mjs @@ -0,0 +1,16 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember/object/mixin', // type only + '@ember/debug', // assert, deprecate +]; + +export const entryPoints = ['./src/-private.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/holodeck/.gitignore b/packages/holodeck/.gitignore new file mode 100644 index 00000000000..3d75225a1b4 --- /dev/null +++ b/packages/holodeck/.gitignore @@ -0,0 +1,172 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# IntelliJ based IDEs +.idea diff --git a/packages/holodeck/CHANGELOG.md b/packages/holodeck/CHANGELOG.md new file mode 100644 index 00000000000..20d2db55168 --- /dev/null +++ b/packages/holodeck/CHANGELOG.md @@ -0,0 +1,59 @@ +# @warp-drive/holodeck Changelog + +## v0.0.0-alpha.71 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9383](https://github.com/emberjs/data/pull/9383) fix: ensure cache-handler clones full errors ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v0.0.0-alpha.9 (2024-02-24) + +#### :rocket: Enhancement + +* [#8921](https://github.com/emberjs/data/pull/8921) feat: Improved Fetch Errors ([@runspired](https://github.com/runspired)) + +#### :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)) +* [#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)) +* [#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)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@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)) +* [#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: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/holodeck/LICENSE.md b/packages/holodeck/LICENSE.md new file mode 100644 index 00000000000..ee1ae5bf425 --- /dev/null +++ b/packages/holodeck/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (C) 2023 EmberData and WarpDrive contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/holodeck/NCC-1701-a-blue.svg b/packages/holodeck/NCC-1701-a-blue.svg new file mode 100644 index 00000000000..3b46f232c1a --- /dev/null +++ b/packages/holodeck/NCC-1701-a-blue.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/holodeck/NCC-1701-a.svg b/packages/holodeck/NCC-1701-a.svg new file mode 100644 index 00000000000..8ee688dcf30 --- /dev/null +++ b/packages/holodeck/NCC-1701-a.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/holodeck/README.md b/packages/holodeck/README.md new file mode 100644 index 00000000000..4a8263a486a --- /dev/null +++ b/packages/holodeck/README.md @@ -0,0 +1,301 @@ +

+ + +

+ +

⚡️ Simple, Fast HTTP Mocking

+

Ideal for Test Suites

+ +

+ WarpDrive Holodeck +

+ + +- ⚡️ Real network requests + - brotli compression + - http/2 + - no CORS preflight requests +- 💜 Unparalleled DX + - debug real network requests + - every request is scoped to a test + - run as many tests as desired simultaneously +- 🔥 Blazing Fast Tests + - record your tests when you change them + - replays from cache until you change them again + - zero-work: setup work is skipped when in replay mode + +## Installation + + +```json +pnpm install @warp-drive/holodeck +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + + +## Usage +#### Mocking from Within a Test + +```ts +import { GET } from '@warp-drive/holodeck/mock'; + +await GET(context, 'users/1', () => ({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Chris Thoburn', + }, + }, + +// set RECORD to false or remove +// the options hash entirely once the request +// has been recorded +}), { RECORD: true }); +``` + +## Motivations + +Comprehensive DX around data management should extend to testing. + +### ✨ Amazing Developer Experience + +EmberData already understands your data schemas. Building a mocking utility with tight integration into your data usage patterns could bring enormous DX and test suite performance benefits. + +Building a real mock server instead of intercepting requests in the browser or via ServiceWorker gives us out-of-the-box DX, better tunability, and greater ability to optimize test suite performance. Speed is the ultimate DX. + +### 🔥 Blazing Fast Tests + +We've noticed test suites spending an enormous amount of time creating and tearing down mock state in between tests. To combat this, we want to provide +an approach built over `http/3` (`http/2` for now) utilizing aggressive caching +and `brotli` minification in a way that can be replayed over and over again. + +Basically, pay the cost when you write the test. Forever after skip the cost until you need to edit the test again. + +## Setup + +### Use with WarpDrive + +First, you will need to add the holodeck handler to the request manager chain prior to `Fetch` (or any equivalent handler that proceeds to network). + +For instance: + +```ts +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import { MockServerHandler } from '@warp-drive/holodeck'; + +const manager = new RequestManager(); +manager.use([new MockServerHandler(testContext), Fetch]); +``` + +From within a test this might look like: + +```ts +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import { MockServerHandler } from '@warp-drive/holodeck'; +import { module, test } from 'qunit'; + +module('my module', function() { + test('my test', async function() { + const manager = new RequestManager(); + manager.use([new MockServerHandler(this), Fetch]); + }); +}); +``` + +Next, you will need to configure holodeck to understand your tests contexts. For qunit and diagnostic +in a project using Ember this is typically done in `tests/test-helper.js` + +#### With Diagnostic + +```ts +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { setConfig, setTestId } from '@warp-drive/holodeck'; + +// if not proxying the port / set port to the correct value here +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; + +setConfig({ host: MockHost }); + +setupGlobalHooks((hooks) => { + hooks.beforeEach(function (assert) { + setTestId(this, assert.test.testId); + }); + hooks.afterEach(function () { + setTestId(this, null); + }); +}); +``` + +#### With QUnit + +```ts +import * as QUnit from 'qunit'; +import { setConfig, setTestId } from '@warp-drive/holodeck'; + +// if not proxying the port / set port to the correct value here +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; + +setConfig({ host: MockHost }); + +QUnit.hooks.beforeEach(function (assert) { + setTestId(assert.test.testId); +}); +QUnit.hooks.afterEach(function (assert) { + setTestId(null); +}); +``` + +### Testem + +You can integrate holodeck with Testem using testem's [async config capability](https://github.com/testem/testem/blob/master/docs/config_file.md#returning-a-promise-from-testemjs): + +```ts +module.exports = async function () { + const holodeck = (await import('@warp-drive/holodeck')).default; + await holodeck.launchProgram({ + port: 7373, + }); + + process.on('beforeExit', async () => { + await holodeck.endProgram(); + }); + + return { + // ... testem config + }; +}; +``` + +If you need the API mock to run on the same port as the test suite, you can use Testem's [API Proxy](https://github.com/testem/testem/tree/master?tab=readme-ov-file#api-proxy) + +```ts +module.exports = async function () { + const holodeck = (await import('@warp-drive/holodeck')).default; + await holodeck.launchProgram({ + port: 7373, + }); + + process.on('beforeExit', async () => { + await holodeck.endProgram(); + }); + + return { + "proxies": { + "/api": { + // holodeck always runs on https + // the proxy is transparent so this means /api/v1 will route to https://localhost:7373/api/v1 + "target": "https://localhost:7373", + // "onlyContentTypes": ["xml", "json"], + // if test suite is on http, set this to false + // "secure": false, + }, + } + }; +}; +``` + +### Diagnostic + +holodeck can be launched and cleaned up using the lifecycle hooks in the launch config +for diagnostic in `diagnostic.js`: + +```ts +import launch from '@warp-drive/diagnostic/server/default-setup.js'; +import holodeck from '@warp-drive/holodeck'; + +await launch({ + async setup(options) { + await holodeck.launchProgram({ + port: options.port + 1, + }); + }, + async cleanup() { + await holodeck.endProgram(); + }, +}); +``` + +### ♥️ Credits + +
+ Brought to you with ♥️ love by 🐹 Ember + + +
diff --git a/packages/holodeck/babel.config.mjs b/packages/holodeck/babel.config.mjs new file mode 100644 index 00000000000..1cefd69a479 --- /dev/null +++ b/packages/holodeck/babel.config.mjs @@ -0,0 +1,8 @@ +export default { + plugins: [ + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/holodeck/eslint.config.mjs b/packages/holodeck/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/holodeck/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/holodeck/package.json b/packages/holodeck/package.json new file mode 100644 index 00000000000..5bfcdf81598 --- /dev/null +++ b/packages/holodeck/package.json @@ -0,0 +1,94 @@ +{ + "name": "@warp-drive/holodeck", + "description": "⚡️ Simple, Fast HTTP Mocking for Tests", + "version": "4.12.8", + "private": true, + "license": "MIT", + "author": "Chris Thoburn ", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/holodeck" + }, + "homepage": "https://github.com/emberjs/data", + "bugs": "https://github.com/emberjs/data/issues", + "engines": { + "node": ">= 18.20.4" + }, + "keywords": [ + "http-mock" + ], + "volta": { + "extends": "../../package.json" + }, + "dependencies": { + "@hono/node-server": "^1.11.1", + "chalk": "^5.3.0", + "hono": "^4.6.5" + }, + "type": "module", + "files": [ + "bin", + "dist", + "README.md", + "LICENSE.md", + "server", + "unstable-preview-types", + "NCC-1701-a.svg", + "NCC-1701-a-blue.svg" + ], + "bin": { + "ensure-cert": "./server/ensure-cert.js" + }, + "scripts": { + "check:pkg-types": "tsc --noEmit", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "peerDependencies": { + "@ember-data/request": "workspace:*", + "@warp-drive/core-types": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@babel/runtime": "^7.24.5", + "@ember-data/request": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "exports": { + ".": { + "node": "./server/index.js", + "bun": "./server/index.js", + "deno": "./server/index.js", + "browser": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "import": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "default": "./server/index.js" + }, + "./mock": { + "types": "./unstable-preview-types/mock.d.ts", + "default": "./dist/mock.js" + } + }, + "dependenciesMeta": { + "@ember-data/request": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + } + } +} diff --git a/packages/holodeck/pnpm-install-logo.png b/packages/holodeck/pnpm-install-logo.png new file mode 100644 index 00000000000..e64ddb74e14 Binary files /dev/null and b/packages/holodeck/pnpm-install-logo.png differ diff --git a/packages/holodeck/server/ensure-cert.js b/packages/holodeck/server/ensure-cert.js new file mode 100755 index 00000000000..948c6d47ab4 --- /dev/null +++ b/packages/holodeck/server/ensure-cert.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import { homedir, userInfo } from 'os'; +import path from 'path'; + +function getShellConfigFilePath() { + const shell = userInfo().shell; + switch (shell) { + case '/bin/zsh': + return path.join(homedir(), '.zshrc'); + case '/bin/bash': + return path.join(homedir(), '.bashrc'); + default: + throw Error( + `Unable to determine configuration file for shell: ${shell}. Manual SSL Cert Setup Required for Holodeck.` + ); + } +} + +function main() { + let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH; + let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH; + const configFilePath = getShellConfigFilePath(); + + if (!CERT_PATH || !KEY_PATH) { + console.log(`Environment variables not found, updating the environment config file...\n`); + + if (!CERT_PATH) { + CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem'); + process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH; + execSync(`echo '\nexport HOLODECK_SSL_CERT_PATH="${CERT_PATH}"' >> ${configFilePath}`); + console.log(`Added HOLODECK_SSL_CERT_PATH to ${configFilePath}`); + } + + if (!KEY_PATH) { + KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem'); + process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH; + execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`); + console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`); + } + + console.log( + `\n*** Please restart your terminal session to apply the changes or run \`source ${configFilePath}\`. ***\n` + ); + } + + if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) { + console.log('SSL certificate or key not found, generating new ones...'); + + execSync(`mkcert -install`); + execSync(`mkcert -key-file ${KEY_PATH} -cert-file ${CERT_PATH} localhost`); + + console.log('SSL certificate and key generated.'); + } else { + console.log('SSL certificate and key found, using existing.'); + } + + console.log(`Certificate path: ${CERT_PATH}`); + console.log(`Key path: ${KEY_PATH}`); +} + +main(); diff --git a/packages/holodeck/server/index.js b/packages/holodeck/server/index.js new file mode 100644 index 00000000000..69c2ffb9cc1 --- /dev/null +++ b/packages/holodeck/server/index.js @@ -0,0 +1,383 @@ +/* global Bun */ +import { serve } from '@hono/node-server'; +import chalk from 'chalk'; +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { HTTPException } from 'hono/http-exception'; +import { logger } from 'hono/logger'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import http2 from 'node:http2'; +import zlib from 'node:zlib'; +import { homedir } from 'os'; +import path from 'path'; + +/** @type {import('bun-types')} */ +const isBun = typeof Bun !== 'undefined'; +const DEBUG = process.env.DEBUG?.includes('holodeck') || process.env.DEBUG === '*'; +const CURRENT_FILE = new URL(import.meta.url).pathname; + +function getCertInfo() { + let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH; + let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH; + + if (!CERT_PATH) { + CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem'); + process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH; + + console.log( + `HOLODECK_SSL_CERT_PATH was not found in the current environment. Setting it to default value of ${CERT_PATH}` + ); + } + + if (!KEY_PATH) { + KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem'); + process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH; + + console.log( + `HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}` + ); + } + + if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) { + throw new Error('SSL certificate or key not found, you may need to run `pnpx @warp-drive/holodeck ensure-cert`'); + } + + return { + CERT_PATH, + KEY_PATH, + CERT: fs.readFileSync(CERT_PATH), + KEY: fs.readFileSync(KEY_PATH), + }; +} + +const DEFAULT_PORT = 1135; +const BROTLI_OPTIONS = { + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + // brotli currently defaults to 11 but lets be explicit + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, + }, +}; +function compress(code) { + return zlib.brotliCompressSync(code, BROTLI_OPTIONS); +} + +/** + * removes the protocol, host, and port from a url + */ +function getNiceUrl(url) { + const urlObj = new URL(url); + urlObj.searchParams.delete('__xTestId'); + urlObj.searchParams.delete('__xTestRequestNumber'); + return (urlObj.pathname + urlObj.searchParams.toString()).slice(1); +} + +/* +{ + projectRoot: string; + testId: string; + url: string; + method: string; + body: string; + testRequestNumber: number +} +*/ +function generateFilepath(options) { + const { body } = options; + const bodyHash = body ? crypto.createHash('md5').update(body).digest('hex') : null; + const cacheDir = generateFileDir(options); + return `${cacheDir}/${bodyHash ? `${bodyHash}-` : 'res'}`; +} +function generateFileDir(options) { + const { projectRoot, testId, url, method, testRequestNumber } = options; + return `${projectRoot}/.mock-cache/${testId}/${method}-${testRequestNumber}-${url}`; +} + +function replayRequest(context, cacheKey) { + let meta; + try { + meta = fs.readFileSync(`${cacheKey}.meta.json`, 'utf-8'); + } catch (e) { + context.header('Content-Type', 'application/vnd.api+json'); + context.status(400); + return context.body( + JSON.stringify({ + errors: [ + { + status: '400', + code: 'MOCK_NOT_FOUND', + title: 'Mock not found', + detail: `No mock found for ${context.req.method} ${context.req.url}. You may need to record a mock for this request.`, + }, + ], + }) + ); + } + + const metaJson = JSON.parse(meta); + const bodyPath = `${cacheKey}.body.br`; + + const headers = new Headers(metaJson.headers || {}); + const bodyInit = metaJson.status !== 204 && metaJson.status < 500 ? fs.createReadStream(bodyPath) : ''; + const response = new Response(bodyInit, { + status: metaJson.status, + statusText: metaJson.statusText, + headers, + }); + + if (metaJson.status > 400) { + throw new HTTPException(metaJson.status, { res: response, message: metaJson.statusText }); + } + + return response; +} + +function createTestHandler(projectRoot) { + const TestHandler = async (context) => { + try { + const { req } = context; + + const testId = req.query('__xTestId'); + const testRequestNumber = req.query('__xTestRequestNumber'); + const niceUrl = getNiceUrl(req.url); + + if (!testId) { + context.header('Content-Type', 'application/vnd.api+json'); + context.status(400); + return context.body( + JSON.stringify({ + errors: [ + { + status: '400', + code: 'MISSING_X_TEST_ID_HEADER', + title: 'Request to the http mock server is missing the `X-Test-Id` header', + detail: + "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.", + source: { header: 'X-Test-Id' }, + }, + ], + }) + ); + } + + if (!testRequestNumber) { + context.header('Content-Type', 'application/vnd.api+json'); + context.status(400); + return context.body( + JSON.stringify({ + errors: [ + { + status: '400', + code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER', + title: 'Request to the http mock server is missing the `X-Test-Request-Number` header', + detail: + "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.", + source: { header: 'X-Test-Request-Number' }, + }, + ], + }) + ); + } + + if (req.method === 'POST' || niceUrl === '__record') { + const payload = await req.json(); + const { url, headers, method, status, statusText, body, response } = payload; + const cacheKey = generateFilepath({ + projectRoot, + testId, + url, + method, + body: body ? JSON.stringify(body) : null, + testRequestNumber, + }); + // allow Content-Type to be overridden + headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json'; + // We always compress and chunk the response + headers['Content-Encoding'] = 'br'; + // we don't cache since tests will often reuse similar urls for different payload + headers['Cache-Control'] = 'no-store'; + + const cacheDir = generateFileDir({ + projectRoot, + testId, + url, + method, + testRequestNumber, + }); + + fs.mkdirSync(cacheDir, { recursive: true }); + fs.writeFileSync( + `${cacheKey}.meta.json`, + JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2) + ); + fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response))); + context.status(204); + return context.body(null); + } else { + const body = await req.text(); + const cacheKey = generateFilepath({ + projectRoot, + testId, + url: niceUrl, + method: req.method, + body, + testRequestNumber, + }); + return replayRequest(context, cacheKey); + } + } catch (e) { + if (e instanceof HTTPException) { + throw e; + } + context.header('Content-Type', 'application/vnd.api+json'); + context.status(500); + return context.body( + JSON.stringify({ + errors: [ + { + status: '500', + code: 'MOCK_SERVER_ERROR', + title: 'Mock Server Error during Request', + detail: e.message, + }, + ], + }) + ); + } + }; + + return TestHandler; +} + +/* +{ port?: number, projectRoot: string } +*/ +export function createServer(options) { + const app = new Hono(); + if (DEBUG) { + app.use('*', logger()); + } + app.use( + '*', + cors({ + origin: (origin) => + origin.startsWith('http://localhost:') || origin.startsWith('https://localhost:') ? origin : '*', + allowHeaders: ['Accept', 'Content-Type'], + allowMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE', 'PATCH'], + exposeHeaders: ['Content-Length', 'Content-Type'], + maxAge: 60_000, + credentials: false, + }) + ); + app.all('*', createTestHandler(options.projectRoot)); + + const { CERT, KEY } = getCertInfo(); + + serve({ + fetch: app.fetch, + createServer: (_, requestListener) => { + try { + return http2.createSecureServer( + { + key: KEY, + cert: CERT, + }, + requestListener + ); + } catch (e) { + console.log(chalk.yellow(`Failed to create secure server, falling back to http server. Error: ${e.message}`)); + return http2.createServer(requestListener); + } + }, + port: options.port ?? DEFAULT_PORT, + hostname: 'localhost', + // bun uses TLS options + // tls: { + // key: Bun.file(KEY_PATH), + // cert: Bun.file(CERT_PATH), + // }, + }); + + console.log( + `\tMock server running at ${chalk.magenta('https://localhost:') + chalk.yellow(options.port ?? DEFAULT_PORT)}` + ); +} + +const servers = new Map(); + +export default { + async launchProgram(config = {}) { + const projectRoot = process.cwd(); + const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then( + (pkg) => pkg.name + ); + const options = { name, projectRoot, ...config }; + console.log( + chalk.grey( + `\n\t@${chalk.greenBright('warp-drive')}/${chalk.magentaBright( + 'holodeck' + )} 🌅\n\t=================================\n` + ) + + chalk.grey( + `\n\tHolodeck Access Granted\n\t\tprogram: ${chalk.magenta(name)}\n\t\tsettings: ${chalk.green(JSON.stringify(config).split('\n').join(' '))}\n\t\tdirectory: ${chalk.cyan(projectRoot)}\n\t\tengine: ${chalk.cyan( + isBun ? 'bun@' + Bun.version : 'node' + )}` + ) + ); + console.log(chalk.grey(`\n\tStarting Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`)); + + if (isBun) { + const serverProcess = Bun.spawn( + ['node', '--experimental-default-type=module', CURRENT_FILE, JSON.stringify(options)], + { + env: process.env, + cwd: process.cwd(), + stdout: 'inherit', + stderr: 'inherit', + } + ); + servers.set(projectRoot, serverProcess); + return; + } + + if (servers.has(projectRoot)) { + throw new Error(`Holodeck is already running for project '${name}' at '${projectRoot}'`); + } + + servers.set(projectRoot, createServer(options)); + }, + async endProgram() { + console.log(chalk.grey(`\n\tEnding Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`)); + const projectRoot = process.cwd(); + + if (!servers.has(projectRoot)) { + const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then( + (pkg) => pkg.name + ); + throw new Error(`Holodeck was not running for project '${name}' at '${projectRoot}'`); + } + + if (isBun) { + const serverProcess = servers.get(projectRoot); + serverProcess.kill(); + return; + } + + servers.get(projectRoot).close(); + servers.delete(projectRoot); + }, +}; + +function main() { + const args = process.argv.slice(); + if (!isBun && args.length) { + if (args[1] !== CURRENT_FILE) { + return; + } + const options = JSON.parse(args[2]); + createServer(options); + } +} + +main(); diff --git a/packages/holodeck/src/index.ts b/packages/holodeck/src/index.ts new file mode 100644 index 00000000000..98a2008bce3 --- /dev/null +++ b/packages/holodeck/src/index.ts @@ -0,0 +1,86 @@ +import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request'; + +import type { ScaffoldGenerator } from './mock'; + +const TEST_IDS = new WeakMap(); + +let HOST = 'https://localhost:1135/'; +export function setConfig({ host }: { host: string }) { + HOST = host.endsWith('/') ? host : `${host}/`; +} + +export function setTestId(context: object, str: string | null) { + if (str && TEST_IDS.has(context)) { + throw new Error(`MockServerHandler is already configured with a testId.`); + } + if (str) { + TEST_IDS.set(context, { id: str, request: 0, mock: 0 }); + } else { + TEST_IDS.delete(context); + } +} + +let IS_RECORDING = false; +export function setIsRecording(value: boolean) { + IS_RECORDING = Boolean(value); +} +export function getIsRecording() { + return IS_RECORDING; +} + +export class MockServerHandler implements Handler { + declare owner: object; + constructor(owner: object) { + this.owner = owner; + } + async request(context: RequestContext, next: NextFn): Promise> { + const test = TEST_IDS.get(this.owner); + if (!test) { + throw new Error( + `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test` + ); + } + + const request: RequestInfo = Object.assign({}, context.request); + const isRecording = request.url!.endsWith('/__record'); + const firstChar = request.url!.includes('?') ? '&' : '?'; + const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${ + isRecording ? test.mock++ : test.request++ + }`; + request.url = request.url + queryForTest; + + request.mode = 'cors'; + request.credentials = 'omit'; + request.referrerPolicy = ''; + + try { + const future = next(request); + context.setStream(future.getStream()); + return await future; + } catch (e) { + if (e instanceof Error && !(e instanceof DOMException)) { + e.message = e.message.replace(queryForTest, ''); + } + throw e; + } + } +} + +export async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) { + const test = TEST_IDS.get(owner); + if (!test) { + throw new Error(`Cannot call "mock" before configuring a testId. Use setTestId to set the testId for each test`); + } + const testMockNum = test.mock++; + if (getIsRecording() || isRecording) { + const port = window.location.port ? `:${window.location.port}` : ''; + const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`; + await fetch(url, { + method: 'POST', + body: JSON.stringify(generate()), + mode: 'cors', + credentials: 'omit', + referrerPolicy: '', + }); + } +} diff --git a/packages/holodeck/src/mock.ts b/packages/holodeck/src/mock.ts new file mode 100644 index 00000000000..1d2697ef23c --- /dev/null +++ b/packages/holodeck/src/mock.ts @@ -0,0 +1,57 @@ +import { getIsRecording, mock } from '.'; + +export interface Scaffold { + status: number; + statusText?: string; + headers: Record; + body: Record | string | null; + method: string; + url: string; + response: Record; +} + +export type ScaffoldGenerator = () => Scaffold; +export type ResponseGenerator = () => Record; + +/** + * Sets up Mocking for a GET request on the mock server + * for the supplied url. + * + * The response body is generated by the supplied response function. + * + * Available options: + * - status: the status code to return (default: 200) + * - headers: the headers to return (default: {}) + * - body: the body to match against for the request (default: null) + * - RECORD: whether to record the request (default: false) + * + * @param url the url to mock, relative to the mock server host (e.g. `users/1`) + * @param response a function which generates the response to return + * @param options status, headers for the response, body to match against for the request, and whether to record the request + * @return + */ +export function GET( + owner: object, + url: string, + response: ResponseGenerator, + options?: Partial> & { RECORD?: boolean } +): Promise { + return mock( + owner, + () => ({ + status: options?.status ?? 200, + statusText: options?.statusText ?? 'OK', + headers: options?.headers ?? {}, + body: options?.body ?? null, + method: 'GET', + url, + response: response(), + }), + getIsRecording() || (options?.RECORD ?? false) + ); +} +export function POST() {} +export function PUT() {} +export function PATCH() {} +export function DELETE() {} +export function QUERY() {} diff --git a/packages/holodeck/tsconfig.json b/packages/holodeck/tsconfig.json new file mode 100644 index 00000000000..6454af93255 --- /dev/null +++ b/packages/holodeck/tsconfig.json @@ -0,0 +1,54 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + "composite": true, + "incremental": true, + "rootDir": "src", + // Support generation of source maps. Note: you must *also* enable source + // maps in your `ember-cli-babel` config and/or `babel.config.js`. + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "baseUrl": ".", + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../core-types" + } + ] +} diff --git a/packages/holodeck/vite.config.mjs b/packages/holodeck/vite.config.mjs new file mode 100644 index 00000000000..56e2bc98d16 --- /dev/null +++ b/packages/holodeck/vite.config.mjs @@ -0,0 +1,13 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = []; +export const entryPoints = ['./src/index.ts', './src/mock.ts']; + +export default createConfig( + { + entryPoints, + externals, + fixModule: false, + }, + import.meta.resolve +); diff --git a/packages/json-api/.npmignore b/packages/json-api/.npmignore deleted file mode 100644 index e4bce62a5ec..00000000000 --- a/packages/json-api/.npmignore +++ /dev/null @@ -1,40 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/node-tests - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/json-api/CHANGELOG.md b/packages/json-api/CHANGELOG.md new file mode 100644 index 00000000000..3cdc13871fb --- /dev/null +++ b/packages/json-api/CHANGELOG.md @@ -0,0 +1,90 @@ +# @ember-data/json-api Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@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)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@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)) +* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9355](https://github.com/emberjs/data/pull/9355) Fix: @attr defaultValue() results should persist after initialization ([@christophersansone](https://github.com/christophersansone)) +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9476](https://github.com/emberjs/data/pull/9476) chore: cleanup symbol usage ([@runspired](https://github.com/runspired)) +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (4) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) +Christopher Sansone ([@christophersansone](https://github.com/christophersansone)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9161](https://github.com/emberjs/data/pull/9161) docs: fix return signature of peekRequest ([@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)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@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)) +* [#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)) +* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@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)) + +#### :bug: Bug Fix + +* [#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)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) +* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@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: (4) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +[@BoussonKarel](https://github.com/BoussonKarel) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) +Jared Galanis ([@jaredgalanis](https://github.com/jaredgalanis)) + diff --git a/packages/json-api/README.md b/packages/json-api/README.md index dedd31668f1..282cc90917e 100644 --- a/packages/json-api/README.md +++ b/packages/json-api/README.md @@ -15,9 +15,9 @@ />

-

Provides an in-memory JSON:API document and resource cache implementation

+

Elegantly composable. Made for JSON:API

-This package provides an [*Ember***Data** Cache](https://github.com/emberjs/data/blob/main/ember-data-types/cache/cache.ts) implementation for [JSON:API](https://jsonapi.org/) +This package provides an in-memory document and resource [Cache](https://github.com/emberjs/data/blob/main/ember-data-types/cache/cache.ts) and associated utilities for use with [*Ember***Data**](https://github.com/emberjs/data/) and [JSON:API](https://jsonapi.org/). ## Installation @@ -27,9 +27,23 @@ Install using your javascript package manager of choice. For instance with [pnpm pnpm add @ember-data/json-api ``` +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/json-api/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/json-api/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/json-api/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/json-api/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/json-api/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + +## Getting Started + +If this package is how you are first learning about EmberData, we recommend starting with learning about the [Store](https://github.com/emberjs/data/blob/main/packages/store/README.md) and [Requests](https://github.com/emberjs/data/blob/main/packages/request/README.md) + ## 🚀 Setup -> **Note** When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below +> **Note** +> When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below > configuration is handled for you automatically. ```ts @@ -91,3 +105,42 @@ class extends Store { ``` For the full list of APIs available read the code documentation for [*Ember***Data** Cache](https://github.com/emberjs/data/blob/main/ember-data-types/cache/cache.ts) + +## Request Builders + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/json-api/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/* + => { + url: 'https://api.example.com/v1/ember-developers/1?include=friends,pets', + method: 'GET', + headers: , + // => 'Accept': 'application/vnd.api+json' + // => 'Content-Type': 'application/vnd.api+json' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +*/ +``` + +Request builder output may be used with either `requestManager.request` or `store.request`. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common JSON:API format (dasherized pluralized resource types). + +### Available Builders + +- [createRecord]() +- [deleteRecord]() +- [findRecord]() +- [query]() +- [updateRecord]() diff --git a/packages/json-api/addon-main.cjs b/packages/json-api/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/json-api/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/json-api/addon-main.js b/packages/json-api/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/json-api/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/json-api/babel.config.js b/packages/json-api/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/json-api/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/json-api/babel.config.mjs b/packages/json-api/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/json-api/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/json-api/eslint.config.mjs b/packages/json-api/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/json-api/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/json-api/package.json b/packages/json-api/package.json index 9752960bd99..eff6132c4c1 100644 --- a/packages/json-api/package.json +++ b/packages/json-api/package.json @@ -8,77 +8,100 @@ "repository": { "type": "git", "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "packages/record-data" + "directory": "packages/json-api" }, "license": "MIT", "author": "", "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], - "peerDependencies": { - "@ember-data/store": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8" + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@warp-drive/core-types": { "injected": true }, "@ember-data/graph": { "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true } }, + "peerDependencies": { + "@ember-data/graph": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@warp-drive/core-types": "workspace:*" + }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "ember-cli-babel": "^7.26.11" + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/graph": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "typescript": "^5.0.3", - "walk-sync": "^3.0.0", - "webpack": "^5.77.0" + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "expect-type": "^0.20.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "ember": { "edition": "octane" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/json-api/rollup.config.mjs b/packages/json-api/rollup.config.mjs deleted file mode 100644 index 98c1589c911..00000000000 --- a/packages/json-api/rollup.config.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@ember-data/store/-private', - '@ember-data/graph/-private', - '@ember/service', - 'ember-inflector', - '@ember/debug', - '@ember/string', - '@ember/object', - '@ember/object/mixin', - '@ember/application', - '@glimmer/env', - '@ember/runloop', - '@ember/polyfills', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/json-api/src/-private/builders/-utils.ts b/packages/json-api/src/-private/builders/-utils.ts new file mode 100644 index 00000000000..937b294e180 --- /dev/null +++ b/packages/json-api/src/-private/builders/-utils.ts @@ -0,0 +1,266 @@ +/** + * @module @ember-data/json-api/request + */ +import type { BuildURLConfig, UrlOptions } from '@ember-data/request-utils'; +import { buildQueryParams as buildParams, setBuildURLConfig as setConfig } from '@ember-data/request-utils'; +import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { CacheOptions, ConstrainedRequestOptions } from '@warp-drive/core-types/request'; + +export interface JSONAPIConfig extends BuildURLConfig { + profiles?: { + pagination?: string; + [key: string]: string | undefined; + }; + extensions?: { + atomic?: string; + [key: string]: string | undefined; + }; +} + +const JsonApiAccept = 'application/vnd.api+json'; +const DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' }; +export let CONFIG: JSONAPIConfig = DEFAULT_CONFIG; +export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json'; + +/** + * Allows setting extensions and profiles to be used in the `Accept` header. + * + * Extensions and profiles are keyed by their namespace with the value being + * their URI. + * + * Example: + * + * ```ts + * setBuildURLConfig({ + * extensions: { + * atomic: 'https://jsonapi.org/ext/atomic' + * }, + * profiles: { + * pagination: 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination' + * } + * }); + * ``` + * + * This also sets the global configuration for `buildBaseURL` + * for host and namespace values for the application + * in the `@ember-data/request-utils` package. + * + * These values may still be overridden by passing + * them to buildBaseURL directly. + * + * This method may be called as many times as needed + * + * ```ts + * type BuildURLConfig = { + * host: string; + * namespace: string' + * } + * ``` + * + * @method setBuildURLConfig + * @static + * @public + * @for @ember-data/json-api/request + * @param {BuildURLConfig} config + * @return void + */ +export function setBuildURLConfig(config: JSONAPIConfig): void { + CONFIG = Object.assign({}, DEFAULT_CONFIG, config); + + if (config.profiles || config.extensions) { + let accept = JsonApiAccept; + if (config.profiles) { + const profiles = Object.values(config.profiles); + if (profiles.length) { + accept += ';profile="' + profiles.join(' ') + '"'; + } + } + if (config.extensions) { + const extensions = Object.values(config.extensions); + if (extensions.length) { + accept += ';ext=' + extensions.join(' '); + } + } + ACCEPT_HEADER_VALUE = accept; + } + + setConfig(config); +} + +export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void { + if ('host' in options) { + urlOptions.host = options.host; + } + if ('namespace' in options) { + urlOptions.namespace = options.namespace; + } + if ('resourcePath' in options) { + urlOptions.resourcePath = options.resourcePath; + } +} + +export function extractCacheOptions(options: ConstrainedRequestOptions) { + const cacheOptions: CacheOptions = {}; + if ('reload' in options) { + cacheOptions.reload = options.reload; + } + if ('backgroundReload' in options) { + cacheOptions.backgroundReload = options.backgroundReload; + } + return cacheOptions; +} + +interface RelatedObject { + [key: string]: string | string[] | RelatedObject; +} + +export type JsonApiQuery = { + include?: string | string[] | RelatedObject; + fields?: Record; + page?: { + size?: number; + after?: string; + before?: string; + }; +}; + +function isJsonApiQuery(query: JsonApiQuery | QueryParamsSource): query is JsonApiQuery { + if ('include' in query && query.include && typeof query.include === 'object') { + return true; + } + if ('fields' in query || 'page' in query) { + return true; + } + return false; +} + +function collapseIncludePaths(basePath: string, include: RelatedObject, paths: string[]) { + const keys = Object.keys(include); + for (let i = 0; i < keys.length; i++) { + // the key is always included too + paths.push(`${basePath}.${keys[i]}`); + const key = keys[i]; + const value = include[key]; + + // include: { 'company': 'field1,field2' } + if (typeof value === 'string') { + value.split(',').forEach((field) => { + paths.push(`${basePath}.${key}.${field}`); + }); + + // include: { 'company': ['field1', 'field2'] } + } else if (Array.isArray(value)) { + value.forEach((field) => { + paths.push(`${basePath}.${key}.${field}`); + }); + + // include: { 'company': { 'nested': 'field1,field2' } } + } else { + collapseIncludePaths(`${basePath}.${key}`, value, paths); + } + } +} + +/** + * Sorts query params by both key and value, returning a query params string + * + * Treats `included` specially, splicing it into an array if it is a string and sorting the array. + * - If `included` is an object we build paths dynamically for you + * Treats `fields` specially, building JSON:API partial fields params from an object + * Treats `page` specially, building cursor-pagination profile page params from an object + * + * ```ts + * const params = buildQueryParams({ + * include: { + * company: { + * locations: 'address' + * } + * }, + * fields: { + * company: ['name', 'ticker'], + * person: 'name' + * }, + * page: { + * size: 10, + * after: 'abc', + * } + * }); + * + * // => 'fields[company]=name,ticker&fields[person]=name&include=company.locations,company.locations.address&page[after]=abc&page[size]=10' + * ``` + * + * Options: + * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma' + * + * 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2` + * 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2` + * 'repeat': appends the key for every value e.g. `ids=1&ids=2` + * 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2` + * + * @method buildQueryParams + * @static + * @public + * @for @ember-data/json-api/request + * @param {URLSearchParams | object} params + * @param {object} [options] + * @return {string} A sorted query params string without the leading `?` + */ +export function buildQueryParams(query: JsonApiQuery | QueryParamsSource): string { + if (query instanceof URLSearchParams) { + return buildParams(query); + } + + if (!isJsonApiQuery(query)) { + return buildParams(query); + } + + const { include, fields, page, ...rest } = query; + const finalQuery: QueryParamsSource = { + ...rest, + }; + + if ('include' in query) { + // include: { 'company': 'field1,field2' } + // include: { 'company': ['field1', 'field2'] } + // include: { 'company': { 'nested': 'field1,field2' } } + // include: { 'company': { 'nested': ['field1', 'field2'] } } + if (include && !Array.isArray(include) && typeof include === 'object') { + const includePaths: string[] = []; + collapseIncludePaths('', include, includePaths); + finalQuery.include = includePaths.sort(); + + // include: 'field1,field2' + // include: ['field1', 'field2'] + } else { + finalQuery.include = include as string; + } + } + + if (fields) { + const keys = Object.keys(fields).sort(); + for (let i = 0; i < keys.length; i++) { + const resourceType = keys[i]; + const value = fields[resourceType]; + + // fields: { 'company': ['field1', 'field2'] } + if (Array.isArray(value)) { + finalQuery[`fields[${resourceType}]`] = value.sort().join(','); + + // fields: { 'company': 'field1' } + // fields: { 'company': 'field1,field2' } + } else { + finalQuery[`fields[${resourceType}]`] = value.split(',').sort().join(','); + } + } + } + + if (page) { + const keys = Object.keys(page).sort() as Array<'size' | 'after' | 'before'>; + keys.forEach((key) => { + const value = page[key]; + finalQuery[`page[${key}]`] = value!; + }); + } + + return buildParams(finalQuery); +} diff --git a/packages/json-api/src/-private/builders/find-record.ts b/packages/json-api/src/-private/builders/find-record.ts new file mode 100644 index 00000000000..5c7df8a1ac6 --- /dev/null +++ b/packages/json-api/src/-private/builders/find-record.ts @@ -0,0 +1,121 @@ +/** + * @module @ember-data/json-api/request + */ +import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import { pluralize } from '@ember-data/request-utils/string'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { + FindRecordOptions, + FindRecordRequestOptions, + RemotelyAccessibleIdentifier, +} from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +/** + * Builds request options to fetch a single resource by a known id or identifier + * configured for the url and header expectations of most JSON:API APIs. + * + * **Basic Usage** + * + * ```ts + * import { findRecord } from '@ember-data/json-api/request'; + * + * const data = await store.request(findRecord('person', '1')); + * ``` + * + * **With Options** + * + * ```ts + * import { findRecord } from '@ember-data/json-api/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **With an Identifier** + * + * ```ts + * import { findRecord } from '@ember-data/json-api/request'; + * + * const options = findRecord({ type: 'person', id: '1' }, { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { findRecord } from '@ember-data/json-api/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }, { namespace: 'api/v2' }); + * const data = await store.request(options); + * ``` + * + * @method findRecord + * @public + * @static + * @for @ember-data/json-api/request + * @param identifier + * @param options + */ + +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; + +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord( + identifier: RemotelyAccessibleIdentifier, + options?: FindRecordOptions +): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; +export function findRecord( + arg1: string | RemotelyAccessibleIdentifier, + arg2: string | FindRecordOptions | undefined, + arg3?: FindRecordOptions +): FindRecordRequestOptions { + const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1; + const options = ((typeof arg1 === 'string' ? arg3 : arg2) || {}) as FindRecordOptions; + const cacheOptions = extractCacheOptions(options); + const urlOptions: FindRecordUrlOptions = { + identifier, + op: 'findRecord', + resourcePath: pluralize(identifier.type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + + return { + url: options.include?.length + ? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}` + : url, + method: 'GET', + headers, + cacheOptions, + op: 'findRecord', + records: [identifier], + }; +} diff --git a/packages/json-api/src/-private/builders/find-record.type-test.ts b/packages/json-api/src/-private/builders/find-record.type-test.ts new file mode 100644 index 00000000000..605b2d0b90e --- /dev/null +++ b/packages/json-api/src/-private/builders/find-record.type-test.ts @@ -0,0 +1,61 @@ +import { expectTypeOf } from 'expect-type'; + +import Store from '@ember-data/store'; +import { RequestSignature, type Type } from '@warp-drive/core-types/symbols'; + +import type { FindRecordResultDocument } from './find-record'; +import { findRecord } from './find-record'; + +type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; +}; + +type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; +}; + +type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; +}; + +const store = new Store(); +const query = findRecord('thing', '1'); + +expectTypeOf>(query[RequestSignature]!); + +const result = await store.request(findRecord('thing', '1')); +const result2 = await store.request( + findRecord('thing', '1', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }) +); + +expectTypeOf(result.content); +expectTypeOf(result2.content.data); diff --git a/packages/json-api/src/-private/builders/query.ts b/packages/json-api/src/-private/builders/query.ts new file mode 100644 index 00000000000..5734723ba12 --- /dev/null +++ b/packages/json-api/src/-private/builders/query.ts @@ -0,0 +1,202 @@ +/** + * @module @ember-data/json-api/request + */ +import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; +import { pluralize } from '@ember-data/request-utils/string'; +import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import type { + CacheOptions, + ConstrainedRequestOptions, + PostQueryRequestOptions, + QueryRequestOptions, +} from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +/** + * Builds request options to query for resources, usually by a primary + * type, configured for the url and header expectations of most JSON:API APIs. + * + * The key difference between this and `postQuery` is that this method will send the query + * as query params in the url of a "GET" request instead of as the JSON body of a "POST" + * request. + * + * **Basic Usage** + * + * ```ts + * import { query } from '@ember-data/json-api/request'; + * + * const data = await store.request(query('person')); + * ``` + * + * **With Query Params** + * + * ```ts + * import { query } from '@ember-data/json-api/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { query } from '@ember-data/json-api/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }, { reload: true }); + * const data = await store.request(options); + * ``` + * + * @method query + * @public + * @static + * @for @ember-data/json-api/request + * @param identifier + * @param query + * @param options + */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query: QueryParamsSource = {}, + options: ConstrainedRequestOptions = {} +): QueryRequestOptions { + const cacheOptions = extractCacheOptions(options); + const urlOptions: QueryUrlOptions = { + identifier: { type }, + op: 'query', + resourcePath: pluralize(type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + const queryString = buildQueryParams(query, options.urlParamsSettings); + + return { + url: queryString ? `${url}?${queryString}` : url, + method: 'GET', + headers, + cacheOptions, + op: 'query', + }; +} + +/** + * Builds request options to query for resources, usually by a primary + * type, configured for the url and header expectations of most JSON:API APIs. + * + * The key difference between this and `query` is that this method will send the query + * as the JSON body of a "POST" request instead of as query params in the url of a "GET" + * request. + * + * A CacheKey is generated from the url and query params, and used to cache the response + * in the store. + * + * ```ts + * import { postQuery } from '@ember-data/json-api/request'; + * + * const options = postQuery('person', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { postQuery } from '@ember-data/json-api/request'; + * + * const options = postQuery('person', { include: ['pets', 'friends'] }, { reload: true }); + * const data = await store.request(options); + * ``` + * + * @method postQuery + * @public + * @static + * @for @ember-data/json-api/request + * @param identifier + * @param query + * @param options + */ +export function postQuery( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): PostQueryRequestOptions>; +export function postQuery( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): PostQueryRequestOptions; +export function postQuery( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query: QueryParamsSource = {}, + options: ConstrainedRequestOptions = {} +): PostQueryRequestOptions { + const cacheOptions = extractCacheOptions(options); + const urlOptions: QueryUrlOptions = { + identifier: { type }, + op: 'query', + resourcePath: options.resourcePath ?? pluralize(type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + + const queryData = structuredClone(query); + cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`; + + return { + url, + method: 'POST', + body: JSON.stringify(query), + headers, + cacheOptions: cacheOptions as CacheOptions & { key: string }, + op: 'query', + }; +} diff --git a/packages/json-api/src/-private/builders/query.type-test.ts b/packages/json-api/src/-private/builders/query.type-test.ts new file mode 100644 index 00000000000..1cbeef96db9 --- /dev/null +++ b/packages/json-api/src/-private/builders/query.type-test.ts @@ -0,0 +1,71 @@ +import { expectTypeOf } from 'expect-type'; + +import Store from '@ember-data/store'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import { RequestSignature, type Type } from '@warp-drive/core-types/symbols'; + +import { query } from './query'; + +type NoRelations = { + name: string; + [Type]: 'no-relations'; +}; + +type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; +}; + +type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; +}; + +type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; +}; + +const store = new Store(); +const requestInit = query('thing'); + +expectTypeOf>(requestInit[RequestSignature]!); + +const result = await store.request(query('thing', {})); +const query2 = query('thing', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + ], +}); +const result2 = await store.request(query2); + +expectTypeOf(result.content); +expectTypeOf(result2.content.data); + +const result3 = await store.request(query('no-relations', {})); +const result4 = await store.request( + query('no-relations', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + ], + }) +); +const result5 = await store.request(query('no-relations')); + +expectTypeOf(result3.content); +expectTypeOf(result4.content.data); +expectTypeOf(result5.content.data); diff --git a/packages/json-api/src/-private/builders/save-record.ts b/packages/json-api/src/-private/builders/save-record.ts new file mode 100644 index 00000000000..b3883b8af7c --- /dev/null +++ b/packages/json-api/src/-private/builders/save-record.ts @@ -0,0 +1,262 @@ +import { + buildBaseURL, + type CreateRecordUrlOptions, + type DeleteRecordUrlOptions, + type UpdateRecordUrlOptions, +} from '@ember-data/request-utils'; +import { pluralize } from '@ember-data/request-utils/string'; +import { recordIdentifierFor } from '@ember-data/store'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { + ConstrainedRequestOptions, + CreateRequestOptions, + DeleteRequestOptions, + UpdateRequestOptions, +} from '@warp-drive/core-types/request'; + +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions } from './-utils'; + +function isExisting(identifier: StableRecordIdentifier): identifier is StableExistingRecordIdentifier { + return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null; +} + +/** + * Builds request options to delete record for resources, + * configured for the url, method and header expectations of most JSON:API APIs. + * + * **Basic Usage** + * + * ```ts + * import { deleteRecord } from '@ember-data/json-api/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const data = await store.request(deleteRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { deleteRecord } from '@ember-data/json-api/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const options = deleteRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method deleteRecord + * @public + * @static + * @for @ember-data/json-api/request + * @param record + * @param options + */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot delete a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: DeleteRecordUrlOptions = { + identifier: identifier, + op: 'deleteRecord', + resourcePath: pluralize(identifier.type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + + return { + url, + method: 'DELETE', + headers, + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to create new record for resources, + * configured for the url, method and header expectations of most JSON:API APIs. + * + * **Basic Usage** + * + * ```ts + * import { createRecord } from '@ember-data/json-api/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const data = await store.request(createRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { createRecord } from '@ember-data/json-api/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const options = createRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method createRecord + * @public + * @static + * @for @ember-data/json-api/request + * @param record + * @param options + */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + + const urlOptions: CreateRecordUrlOptions = { + identifier: identifier, + op: 'createRecord', + resourcePath: pluralize(identifier.type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + + return { + url, + method: 'POST', + headers, + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to update existing record for resources, + * configured for the url, method and header expectations of most JSON:API APIs. + * + * **Basic Usage** + * + * ```ts + * import { updateRecord } from '@ember-data/json-api/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const data = await store.request(updateRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `patch` - Allows caller to specify whether to use a PATCH request instead of a PUT request, defaults to `false`. + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { updateRecord } from '@ember-data/json-api/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const options = updateRecord(person, { patch: true }); + * const data = await store.request(options); + * ``` + * + * @method updateRecord + * @public + * @static + * @for @ember-data/json-api/request + * @param record + * @param options + */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options: ConstrainedRequestOptions & { patch?: boolean } = {} +): UpdateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot update a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: UpdateRecordUrlOptions = { + identifier: identifier, + op: 'updateRecord', + resourcePath: pluralize(identifier.type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', ACCEPT_HEADER_VALUE); + + return { + url, + method: options.patch ? 'PATCH' : 'PUT', + headers, + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 4b805aab5cd..d954edb2bbb 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -1,50 +1,63 @@ /** * @module @ember-data/json-api */ -import { assert } from '@ember/debug'; -import { schedule } from '@ember/runloop'; - -import { LOG_MUTATIONS, LOG_OPERATIONS } from '@ember-data/debugging'; -import { DEBUG } from '@ember-data/env'; -import { graphFor, peekGraph } from '@ember-data/graph/-private'; -import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/index'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import { type ImmutableRequestInfo, StructuredErrorDocument } from '@ember-data/request/-private/types'; -import type { IdentifierCache } from '@ember-data/store/-private/caches/identifier-cache'; -import type { ResourceBlob } from '@ember-data/types/cache/aliases'; -import type { Change } from '@ember-data/types/cache/change'; +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 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'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { Change } from '@warp-drive/core-types/cache/change'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { ObjectValue, Value } from '@warp-drive/core-types/json/raw'; +import type { + ImmutableRequestInfo, + StructuredDataDocument, + StructuredDocument, + StructuredErrorDocument, +} from '@warp-drive/core-types/request'; +import type { + CollectionField, + FieldSchema, + LegacyRelationshipSchema, + ResourceField, +} from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument, + ResourceDataDocument, ResourceDocument, ResourceErrorDocument, ResourceMetaDocument, SingleResourceDataDocument, - StructuredDataDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper, V2CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; +} from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; import type { CollectionResourceDocument, - CollectionResourceRelationship, ExistingResourceObject, + ResourceObject, SingleResourceDocument, SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { AttributesHash, JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { AttributeSchema, RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { Dict } from '@ember-data/types/q/utils'; - -function isImplicit( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is ImplicitRelationship { +} from '@warp-drive/core-types/spec/json-api-raw'; + +type IdentifierCache = Store['identifierCache']; +type InternalCapabilitiesManager = CacheCapabilitiesManager & { _store: Store }; + +function isImplicit(relationship: GraphEdge): relationship is ImplicitEdge { return relationship.definition.isImplicit; } +function upgradeCapabilities(obj: unknown): asserts obj is InternalCapabilitiesManager {} + const EMPTY_ITERATOR = { iterator() { return { @@ -57,15 +70,22 @@ const EMPTY_ITERATOR = { interface CachedResource { id: string | null; - remoteAttrs: Record | null; - localAttrs: Record | null; - defaultAttrs: Record | null; - inflightAttrs: Record | null; - changes: Record | null; - errors: JsonApiError[] | null; + remoteAttrs: Record | null; + localAttrs: Record | null; + defaultAttrs: Record | null; + inflightAttrs: Record | null; + changes: Record | null; + errors: ApiError[] | null; isNew: boolean; isDeleted: boolean; isDeletionCommitted: boolean; + + /** + * debugging only + * + * @internal + */ + inflightRelationships?: Record | null; } function makeCache(): CachedResource { @@ -116,15 +136,17 @@ export default class JSONAPICache implements Cache { * @property version */ declare version: '2'; - declare __storeWrapper: V2CacheStoreWrapper; + declare _capabilities: CacheCapabilitiesManager; declare __cache: Map; declare __destroyedCache: Map; declare __documents: Map>; + declare __graph: Graph; - constructor(storeWrapper: V2CacheStoreWrapper) { + constructor(capabilities: CacheCapabilitiesManager) { this.version = '2'; - this.__storeWrapper = storeWrapper; + this._capabilities = capabilities; this.__cache = new Map(); + this.__graph = graphFor(capabilities); this.__destroyedCache = new Map(); this.__documents = new Map(); } @@ -154,7 +176,8 @@ export default class JSONAPICache implements Cache { * }) * ``` * - * > **Note:** The nested `content` and `data` members are not a mistake. This is because + * > **Note** + * > The nested `content` and `data` members are not a mistake. This is because * > there are two separate concepts involved here, the `StructuredDocument` which contains * > the context of a given Request that has been issued with the returned contents as its * > `content` property, and a `JSON:API Document` which is the json contents returned by @@ -167,78 +190,140 @@ export default class JSONAPICache implements Cache { * * @method put * @param {StructuredDocument} doc - * @returns {ResourceDocument} + * @return {ResourceDocument} * @public */ - put(doc: StructuredDocument): SingleResourceDataDocument; - put(doc: StructuredDocument): CollectionResourceDataDocument; + put(doc: StructuredDataDocument): SingleResourceDataDocument; + put(doc: StructuredDataDocument): CollectionResourceDataDocument; put(doc: StructuredErrorDocument): ResourceErrorDocument; put(doc: StructuredDataDocument): ResourceMetaDocument; put(doc: StructuredDocument): ResourceDocument { + assert( + `Expected a JSON:API Document as the content provided to the cache, received ${typeof doc.content}`, + doc instanceof Error || (typeof doc.content === 'object' && doc.content !== null) + ); if (isErrorDocument(doc)) { - return this._putDocument(doc as StructuredErrorDocument); + return this._putDocument(doc, undefined, undefined); } else if (isMetaDocument(doc)) { - return this._putDocument(doc); + return this._putDocument(doc, undefined, undefined); } const jsonApiDoc = doc.content as SingleResourceDocument | CollectionResourceDocument; - let included = jsonApiDoc.included; + const included = jsonApiDoc.included; let i: number, length: number; - const { identifierCache } = this.__storeWrapper; + const { identifierCache } = this._capabilities; + + if (LOG_REQUESTS) { + const Counts = new Map(); + if (included) { + for (i = 0, length = included.length; i < length; i++) { + const type = included[i].type; + Counts.set(type, (Counts.get(type) || 0) + 1); + } + } + if (Array.isArray(jsonApiDoc.data)) { + for (i = 0, length = jsonApiDoc.data.length; i < length; i++) { + const type = jsonApiDoc.data[i].type; + Counts.set(type, (Counts.get(type) || 0) + 1); + } + } else if (jsonApiDoc.data) { + const type = jsonApiDoc.data.type; + Counts.set(type, (Counts.get(type) || 0) + 1); + } + + let str = `JSON:API Cache - put (${doc.content?.lid || doc.request?.url || 'unknown-request'})\n\tContents:`; + Counts.forEach((count, type) => { + str += `\n\t\t${type}: ${count}`; + }); + if (Counts.size === 0) { + str += `\t(empty)`; + } + // eslint-disable-next-line no-console + console.log(str); + } if (included) { for (i = 0, length = included.length; i < length; i++) { - putOne(this, identifierCache, included[i]); + included[i] = putOne(this, identifierCache, included[i]); } } if (Array.isArray(jsonApiDoc.data)) { length = jsonApiDoc.data.length; - let identifiers: StableExistingRecordIdentifier[] = []; + const identifiers: StableExistingRecordIdentifier[] = []; for (i = 0; i < length; i++) { identifiers.push(putOne(this, identifierCache, jsonApiDoc.data[i])); } - return this._putDocument(doc as StructuredDataDocument, identifiers); + return this._putDocument( + doc as StructuredDataDocument, + identifiers, + included as StableExistingRecordIdentifier[] + ); } if (jsonApiDoc.data === null) { - return this._putDocument(doc as StructuredDataDocument, null); + return this._putDocument( + doc as StructuredDataDocument, + null, + included as StableExistingRecordIdentifier[] + ); } assert( - `Expected an object in the 'data' property in a call to 'push', but was ${typeof jsonApiDoc.data}`, + `Expected a resource object in the 'data' property in the document provided to the cache, but was ${typeof jsonApiDoc.data}`, typeof jsonApiDoc.data === 'object' ); - let identifier = putOne(this, identifierCache, jsonApiDoc.data); - return this._putDocument(doc as StructuredDataDocument, identifier); + const identifier = putOne(this, identifierCache, jsonApiDoc.data); + return this._putDocument( + doc as StructuredDataDocument, + identifier, + included as StableExistingRecordIdentifier[] + ); } - _putDocument(doc: StructuredErrorDocument): ResourceErrorDocument; - _putDocument(doc: StructuredDataDocument): ResourceMetaDocument; + _putDocument( + doc: StructuredErrorDocument, + data: undefined, + included: undefined + ): ResourceErrorDocument; + _putDocument( + doc: StructuredDataDocument, + data: undefined, + included: undefined + ): ResourceMetaDocument; _putDocument( doc: StructuredDataDocument, - data: StableExistingRecordIdentifier | null + data: StableExistingRecordIdentifier | null, + included: StableExistingRecordIdentifier[] | undefined ): SingleResourceDataDocument; _putDocument( doc: StructuredDataDocument, - data: StableExistingRecordIdentifier[] + data: StableExistingRecordIdentifier[], + included: StableExistingRecordIdentifier[] | undefined ): CollectionResourceDataDocument; _putDocument( doc: StructuredDocument, - data?: StableExistingRecordIdentifier[] | StableExistingRecordIdentifier | null + data: StableExistingRecordIdentifier[] | StableExistingRecordIdentifier | null | undefined, + included: StableExistingRecordIdentifier[] | undefined ): SingleResourceDataDocument | CollectionResourceDataDocument | ResourceErrorDocument | ResourceMetaDocument { // @ts-expect-error narrowing within is just horrible in TS :/ const resourceDocument: SingleResourceDataDocument | CollectionResourceDataDocument | ResourceErrorDocument = isErrorDocument(doc) ? fromStructuredError(doc) : fromBaseDocument(doc); if (data !== undefined) { - (resourceDocument as SingleResourceDataDocument | CollectionResourceDataDocument).data = data; + (resourceDocument as ResourceDataDocument).data = data; + } + + if (included !== undefined) { + assert(`There should not be included data on an Error document`, !isErrorDocument(doc)); + assert(`There should not be included data on a Meta document`, !isMetaDocument(doc)); + (resourceDocument as ResourceDataDocument).included = included; } const request = doc.request as ImmutableRequestInfo | undefined; - const identifier = request ? this.__storeWrapper.identifierCache.getOrCreateDocumentIdentifier(request) : null; + const identifier = request ? this._capabilities.identifierCache.getOrCreateDocumentIdentifier(request) : null; if (identifier) { resourceDocument.lid = identifier.lid; @@ -248,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.__storeWrapper.notifyChange(identifier, hasExisting ? 'updated' : 'added'); + this._capabilities.notifyChange(identifier, hasExisting ? 'updated' : 'added'); } return resourceDocument; @@ -264,15 +349,15 @@ export default class JSONAPICache implements Cache { * @method patch * @public * @param {Operation} op the operation to perform - * @returns {void} + * @return {void} */ patch(op: MergeOperation): void { if (LOG_OPERATIONS) { try { - let _data = JSON.parse(JSON.stringify(op)); + const _data = JSON.parse(JSON.stringify(op)) as object; // eslint-disable-next-line no-console console.log(`EmberData | Operation - patch ${op.op}`, _data); - } catch (e) { + } catch { // eslint-disable-next-line no-console console.log(`EmberData | Operation - patch ${op.op}`, op); } @@ -283,7 +368,7 @@ export default class JSONAPICache implements Cache { this.__cache.set(op.value, cache); this.__cache.delete(op.record); } - graphFor(this.__storeWrapper).update(op, true); + this.__graph.update(op, true); } } @@ -292,21 +377,21 @@ export default class JSONAPICache implements Cache { * * @method mutate * @param {Mutation} mutation - * @returns {void} + * @return {void} * @public */ mutate(mutation: LocalRelationshipOperation): void { if (LOG_MUTATIONS) { try { - let _data = JSON.parse(JSON.stringify(mutation)); + const _data = JSON.parse(JSON.stringify(mutation)) as object; // eslint-disable-next-line no-console console.log(`EmberData | Mutation - update ${mutation.op}`, _data); - } catch (e) { + } catch { // eslint-disable-next-line no-console console.log(`EmberData | Mutation - update ${mutation.op}`, mutation); } } - graphFor(this.__storeWrapper).update(mutation, false); + this.__graph.update(mutation, false); } /** @@ -339,11 +424,11 @@ export default class JSONAPICache implements Cache { * @method peek * @public * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier - * @returns {ResourceDocument | ResourceBlob | null} the known resource data + * @return {ResourceDocument | ResourceObject | null} the known resource data */ - peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableRecordIdentifier): ResourceObject | null; peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { + peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceObject | ResourceDocument | null { if ('type' in identifier) { const peeked = this.__safePeek(identifier, false); @@ -352,20 +437,35 @@ export default class JSONAPICache implements Cache { } const { type, id, lid } = identifier; - const attributes = Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs); - const relationships = {}; + const attributes = Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs) as ObjectValue; + const relationships: ResourceObject['relationships'] = {}; - const graph = graphFor(this.__storeWrapper); - const rels = graph.identifiers.get(identifier); + const rels = this.__graph.identifiers.get(identifier); if (rels) { Object.keys(rels).forEach((key) => { const rel = rels[key]; - if (!rel || rel.definition.isImplicit) { + if (rel.definition.isImplicit) { return; + } else { + relationships[key] = this.__graph.getData(identifier, key); } - relationships[key] = (rel as ManyRelationship | BelongsToRelationship).getData(); }); } + + upgradeCapabilities(this._capabilities); + const store = this._capabilities._store; + const attrs = this._capabilities.schema.fields(identifier); + attrs.forEach((attr, key) => { + if (key in attributes && attributes[key] !== undefined) { + return; + } + const defaultValue = getDefaultValue(attr, identifier, store); + + if (defaultValue !== undefined) { + attributes[key] = defaultValue; + } + }); + return { type, id, @@ -378,18 +478,22 @@ export default class JSONAPICache implements Cache { const document = this.peekRequest(identifier); if (document) { - if ('content' in document) return document.content; + if ('content' in document) return document.content!; } return null; } /** * Peek the Cache for the existing request data associated with - * a cacheable request + * a cacheable request. + * + * This is effectively the reverse of `put` for a request in + * that it will return the the request, response, and content + * whereas `peek` will return just the `content`. * * @method peekRequest * @param {StableDocumentIdentifier} - * @returns {StableDocumentIdentifier | null} + * @return {StructuredDocument | null} * @public */ peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { @@ -404,27 +508,27 @@ export default class JSONAPICache implements Cache { * @param identifier * @param data * @param hasRecord - * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned + * @return {void | string[]} if `hasRecord` is true then calculated key changes should be returned */ upsert( identifier: StableRecordIdentifier, - data: JsonApiResource, - calculateChanges?: boolean | undefined + data: ExistingResourceObject, + calculateChanges?: boolean ): void | string[] { let changedKeys: string[] | undefined; const peeked = this.__safePeek(identifier, false); const existed = !!peeked; const cached = peeked || this._createCache(identifier); - const isLoading = _isLoading(peeked, this.__storeWrapper, identifier) || !recordIsLoaded(peeked); - let isUpdate = !_isEmpty(peeked) && !isLoading; + const isLoading = /*#__NOINLINE__*/ _isLoading(peeked, this._capabilities, identifier) || !recordIsLoaded(peeked); + const isUpdate = /*#__NOINLINE__*/ !_isEmpty(peeked) && !isLoading; if (LOG_OPERATIONS) { try { - let _data = JSON.parse(JSON.stringify(data)); + const _data = JSON.parse(JSON.stringify(data)) as object; // eslint-disable-next-line no-console console.log(`EmberData | Operation - upsert (${existed ? 'merge' : 'insert'})`, _data); - } catch (e) { + } catch { // eslint-disable-next-line no-console console.log(`EmberData | Operation - upsert (${existed ? 'merge' : 'insert'})`, data); } @@ -432,23 +536,26 @@ export default class JSONAPICache implements Cache { if (cached.isNew) { cached.isNew = false; - this.__storeWrapper.notifyChange(identifier, 'identity'); - this.__storeWrapper.notifyChange(identifier, 'state'); + this._capabilities.notifyChange(identifier, 'identity'); + this._capabilities.notifyChange(identifier, 'state'); } if (calculateChanges) { changedKeys = existed ? calculateChangedKeys(cached, data.attributes) : Object.keys(data.attributes || {}); } - cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), data.attributes); + cached.remoteAttrs = Object.assign( + cached.remoteAttrs || (Object.create(null) as Record), + data.attributes + ); if (cached.localAttrs) { if (patchLocalAttributes(cached)) { - this.__storeWrapper.notifyChange(identifier, 'state'); + this._capabilities.notifyChange(identifier, 'state'); } } if (!isUpdate) { - this.__storeWrapper.notifyChange(identifier, 'added'); + this._capabilities.notifyChange(identifier, 'added'); } if (data.id) { @@ -456,11 +563,11 @@ export default class JSONAPICache implements Cache { } if (data.relationships) { - setupRelationships(this.__storeWrapper, identifier, data); + setupRelationships(this.__graph, this._capabilities, identifier, data); } if (changedKeys && changedKeys.length) { - notifyAttributes(this.__storeWrapper, identifier, changedKeys); + notifyAttributes(this._capabilities, identifier, changedKeys); } return changedKeys; @@ -478,7 +585,7 @@ export default class JSONAPICache implements Cache { * * @method fork * @internal - * @returns Promise + * @return Promise */ fork(): Promise { throw new Error(`Not Implemented`); @@ -494,7 +601,7 @@ export default class JSONAPICache implements Cache { * @method merge * @param {Cache} cache * @public - * @returns Promise + * @return Promise */ merge(cache: Cache): Promise { throw new Error(`Not Implemented`); @@ -546,7 +653,7 @@ export default class JSONAPICache implements Cache { * via `cache.hydrate`. * * @method dump - * @returns {Promise} + * @return {Promise} * @public */ dump(): Promise> { @@ -567,7 +674,7 @@ export default class JSONAPICache implements Cache { * * @method hydrate * @param {ReadableStream} stream - * @returns {Promise} + * @return {Promise} * @public */ hydrate(stream: ReadableStream): Promise { @@ -588,44 +695,43 @@ export default class JSONAPICache implements Cache { * @param identifier * @param createArgs */ - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { + clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { if (LOG_MUTATIONS) { try { - let _data = options ? JSON.parse(JSON.stringify(options)) : options; + const _data = options ? (JSON.parse(JSON.stringify(options)) as object) : options; // eslint-disable-next-line no-console console.log(`EmberData | Mutation - clientDidCreate ${identifier.lid}`, _data); - } catch (e) { + } catch { // eslint-disable-next-line no-console console.log(`EmberData | Mutation - clientDidCreate ${identifier.lid}`, options); } } const cached = this._createCache(identifier); cached.isNew = true; - let createOptions = {}; + const createOptions: Record = {}; if (options !== undefined) { - const storeWrapper = this.__storeWrapper; - let attributeDefs = storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier); - let relationshipDefs = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - const graph = graphFor(storeWrapper); - let propertyNames = Object.keys(options); + const storeWrapper = this._capabilities; + const fields = storeWrapper.schema.fields(identifier); + const graph = this.__graph; + const propertyNames = Object.keys(options); for (let i = 0; i < propertyNames.length; i++) { - let name = propertyNames[i]; - let propertyValue = options[name]; + const name = propertyNames[i]; + const propertyValue = options[name]; if (name === 'id') { continue; } - const fieldType: AttributeSchema | RelationshipSchema | undefined = - relationshipDefs[name] || attributeDefs[name]; - let kind = fieldType !== undefined ? ('kind' in fieldType ? fieldType.kind : 'attribute') : null; - let relationship; + const fieldType: FieldSchema | undefined = fields.get(name); + const kind = fieldType !== undefined ? ('kind' in fieldType ? fieldType.kind : 'attribute') : null; + let relationship: ResourceEdge | CollectionEdge; switch (kind) { case 'attribute': this.setAttr(identifier, name, propertyValue); + createOptions[name] = propertyValue; break; case 'belongsTo': this.mutate({ @@ -634,7 +740,7 @@ export default class JSONAPICache implements Cache { record: identifier, value: propertyValue as StableRecordIdentifier | null, }); - relationship = graph.get(identifier, name); + relationship = graph.get(identifier, name) as ResourceEdge; relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; break; @@ -643,9 +749,9 @@ export default class JSONAPICache implements Cache { op: 'replaceRelatedRecords', field: name, record: identifier, - value: propertyValue as StableRecordIdentifier[], + value: propertyValue as unknown as StableRecordIdentifier[], }); - relationship = graph.get(identifier, name); + relationship = graph.get(identifier, name) as CollectionEdge; relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; break; @@ -656,7 +762,7 @@ export default class JSONAPICache implements Cache { } } - this.__storeWrapper.notifyChange(identifier, 'added'); + this._capabilities.notifyChange(identifier, 'added'); return createOptions; } @@ -688,7 +794,6 @@ export default class JSONAPICache implements Cache { earlier changes. If apps do not want this behavior they can either - - chain save requests serially vs allowing concurrent saves - move to using a request handler that caches the inflight state on a per-request basis @@ -698,15 +803,30 @@ export default class JSONAPICache implements Cache { for upsert into the cache. */ if (cached.inflightAttrs) { - if (!cached.localAttrs) { - return; + if (cached.localAttrs) { + Object.assign(cached.inflightAttrs, cached.localAttrs); } - Object.assign(cached.inflightAttrs, cached.localAttrs); - cached.localAttrs = null; - return; + } else { + cached.inflightAttrs = cached.localAttrs; } - cached.inflightAttrs = cached.localAttrs; cached.localAttrs = null; + + if (DEBUG) { + if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + // save off info about saved relationships + const fields = this._capabilities.schema.fields(identifier); + fields.forEach((schema, name) => { + if (schema.kind === 'belongsTo') { + if (this.__graph._isDirty(identifier, name)) { + const relationshipData = this.__graph.getData(identifier, name); + const inFlight = (cached.inflightRelationships = + cached.inflightRelationships || (Object.create(null) as Record)); + inFlight[name] = relationshipData; + } + } + }); + } + } } /** @@ -723,7 +843,7 @@ export default class JSONAPICache implements Cache { result: StructuredDataDocument ): SingleResourceDataDocument { const payload = result.content; - const operation = result.request!.op; + const operation = result.request.op; const data = payload && payload.data; if (!data) { @@ -733,7 +853,7 @@ export default class JSONAPICache implements Cache { ); } - const { identifierCache } = this.__storeWrapper; + const { identifierCache } = this._capabilities; const existingId = committedIdentifier.id; const identifier: StableRecordIdentifier = operation !== 'deleteRecord' && data @@ -742,13 +862,13 @@ export default class JSONAPICache implements Cache { const cached = this.__peek(identifier, false); if (cached.isDeleted) { - graphFor(this.__storeWrapper).push({ + this.__graph.push({ op: 'deleteRecord', record: identifier, isNew: false, }); cached.isDeletionCommitted = true; - this.__storeWrapper.notifyChange(identifier, 'removed'); + this._capabilities.notifyChange(identifier, 'removed'); // TODO @runspired should we early exit here? } @@ -764,13 +884,13 @@ export default class JSONAPICache implements Cache { } cached.isNew = false; - let newCanonicalAttributes: AttributesHash | undefined; + let newCanonicalAttributes: ExistingResourceObject['attributes']; if (data) { if (data.id && !cached.id) { cached.id = data.id; } if (identifier === committedIdentifier && identifier.id !== existingId) { - this.__storeWrapper.notifyChange(identifier, 'identity'); + this._capabilities.notifyChange(identifier, 'identity'); } assert( @@ -779,14 +899,44 @@ export default class JSONAPICache implements Cache { ); if (data.relationships) { - setupRelationships(this.__storeWrapper, identifier, data); + if (DEBUG) { + if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + // assert against bad API behavior where a belongsTo relationship + // is saved but the return payload indicates a different final state. + const fields = this._capabilities.schema.fields(identifier); + fields.forEach((field, name) => { + if (field.kind === 'belongsTo') { + const relationshipData = data.relationships![name]?.data; + if (relationshipData !== undefined) { + const inFlightData = cached.inflightRelationships?.[name] as SingleResourceRelationship; + if (!inFlightData || !('data' in inFlightData)) { + return; + } + const actualData = relationshipData + ? this._capabilities.identifierCache.getOrCreateRecordIdentifier(relationshipData) + : null; + assert( + `Expected the resource relationship '<${identifier.type}>.${name}' on ${ + identifier.lid + } to be saved as ${inFlightData.data ? inFlightData.data.lid : ''} but it was saved as ${ + actualData ? actualData.lid : '' + }`, + inFlightData.data === actualData + ); + } + } + }); + cached.inflightRelationships = null; + } + } + setupRelationships(this.__graph, this._capabilities, identifier, data); } newCanonicalAttributes = data.attributes; } - let changedKeys = calculateChangedKeys(cached, newCanonicalAttributes); + const changedKeys = calculateChangedKeys(cached, newCanonicalAttributes); cached.remoteAttrs = Object.assign( - cached.remoteAttrs || Object.create(null), + cached.remoteAttrs || (Object.create(null) as Record), cached.inflightAttrs, newCanonicalAttributes ); @@ -795,11 +945,11 @@ export default class JSONAPICache implements Cache { if (cached.errors) { cached.errors = null; - this.__storeWrapper.notifyChange(identifier, 'errors'); + this._capabilities.notifyChange(identifier, 'errors'); } - notifyAttributes(this.__storeWrapper, identifier, changedKeys); - this.__storeWrapper.notifyChange(identifier, 'state'); + notifyAttributes(this._capabilities, identifier, changedKeys); + this._capabilities.notifyChange(identifier, 'state'); const included = payload && payload.included; if (included) { @@ -822,12 +972,13 @@ export default class JSONAPICache implements Cache { * @param identifier * @param errors */ - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[] | undefined): void { + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { const cached = this.__peek(identifier, false); if (cached.inflightAttrs) { - let keys = Object.keys(cached.inflightAttrs); + const keys = Object.keys(cached.inflightAttrs); if (keys.length > 0) { - let attrs = (cached.localAttrs = cached.localAttrs || Object.create(null)); + const attrs = (cached.localAttrs = + cached.localAttrs || (Object.create(null) as Record)); for (let i = 0; i < keys.length; i++) { if (attrs[keys[i]] === undefined) { attrs[keys[i]] = cached.inflightAttrs[keys[i]]; @@ -839,7 +990,7 @@ export default class JSONAPICache implements Cache { if (errors) { cached.errors = errors; } - this.__storeWrapper.notifyChange(identifier, 'errors'); + this._capabilities.notifyChange(identifier, 'errors'); } /** @@ -853,7 +1004,7 @@ export default class JSONAPICache implements Cache { * @param identifier */ unloadRecord(identifier: StableRecordIdentifier): void { - const storeWrapper = this.__storeWrapper; + const storeWrapper = this._capabilities; // TODO this is necessary because // we maintain memebership inside InstanceCache // for peekAll, so even though we haven't created @@ -867,7 +1018,16 @@ export default class JSONAPICache implements Cache { const removeFromRecordArray = !this.isDeletionCommitted(identifier); let removed = false; const cached = this.__peek(identifier, false); - peekGraph(storeWrapper)?.unload(identifier); + + if (cached.isNew) { + peekGraph(storeWrapper)?.push({ + op: 'deleteRecord', + record: identifier, + isNew: true, + }); + } else { + peekGraph(storeWrapper)?.unload(identifier); + } // effectively clearing these is ensuring that // we report as `isEmpty` during teardown. @@ -876,13 +1036,13 @@ export default class JSONAPICache implements Cache { cached.defaultAttrs = null; cached.inflightAttrs = null; - let relatedIdentifiers = _allRelatedIdentifiers(storeWrapper, identifier); + const relatedIdentifiers = _allRelatedIdentifiers(storeWrapper, identifier); if (areAllModelsUnloaded(storeWrapper, relatedIdentifiers)) { for (let i = 0; i < relatedIdentifiers.length; ++i) { - let identifier = relatedIdentifiers[i]; - storeWrapper.notifyChange(identifier, 'removed'); + const relatedIdentifier = relatedIdentifiers[i]; + storeWrapper.notifyChange(relatedIdentifier, 'removed'); removed = true; - storeWrapper.disconnectRecord(identifier); + storeWrapper.disconnectRecord(relatedIdentifier); } } @@ -903,11 +1063,10 @@ export default class JSONAPICache implements Cache { * of a test won't cause issues. */ if (this.__destroyedCache.size === 1) { - schedule('destroy', () => { - setTimeout(() => { - this.__destroyedCache.clear(); - }, 100); - }); + // TODO do we still need this? + setTimeout(() => { + this.__destroyedCache.clear(); + }, 100); } if (!removed && removeFromRecordArray) { @@ -925,34 +1084,71 @@ export default class JSONAPICache implements Cache { * @public * @param identifier * @param field - * @returns {unknown} + * @return {unknown} */ - getAttr(identifier: StableRecordIdentifier, attr: string): unknown { - const cached = this.__peek(identifier, true); - assert(`Cannot retrieve attributes for identifier ${identifier} as it is not present in the cache`, cached); + getAttr(identifier: StableRecordIdentifier, attr: string | string[]): Value | undefined { + const isSimplePath = !Array.isArray(attr) || attr.length === 1; + if (Array.isArray(attr) && attr.length === 1) { + attr = attr[0]; + } - // in Prod we try to recover when accessing something that - // doesn't exist - if (!cached) { + if (isSimplePath) { + const attribute = attr as string; + const cached = this.__peek(identifier, true); + assert( + `Cannot retrieve attributes for identifier ${String(identifier)} as it is not present in the cache`, + cached + ); + + // in Prod we try to recover when accessing something that + // doesn't exist + if (!cached) { + return undefined; + } + + if (cached.localAttrs && attribute in cached.localAttrs) { + return cached.localAttrs[attribute]; + } else if (cached.inflightAttrs && attribute in cached.inflightAttrs) { + return cached.inflightAttrs[attribute]; + } else if (cached.remoteAttrs && attribute in cached.remoteAttrs) { + return cached.remoteAttrs[attribute]; + } else if (cached.defaultAttrs && attribute in cached.defaultAttrs) { + return cached.defaultAttrs[attribute]; + } else { + const attrSchema = this._capabilities.schema.fields(identifier).get(attribute); + + upgradeCapabilities(this._capabilities); + const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store); + if (schemaHasLegacyDefaultValueFn(attrSchema)) { + cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record); + cached.defaultAttrs[attribute] = defaultValue; + } + return defaultValue; + } + } + + // TODO @runspired consider whether we need a defaultValue cache in SchemaRecord + // like we do for the simple case above. + const path: string[] = attr as string[]; + const cached = this.__peek(identifier, true); + const basePath = path[0]; + let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : undefined; + if (current === undefined) { + current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : undefined; + } + if (current === undefined) { + current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined; + } + if (current === undefined) { return undefined; } - if (cached.localAttrs && attr in cached.localAttrs) { - return cached.localAttrs[attr]; - } else if (cached.inflightAttrs && attr in cached.inflightAttrs) { - return cached.inflightAttrs[attr]; - } else if (cached.remoteAttrs && attr in cached.remoteAttrs) { - return cached.remoteAttrs[attr]; - } else if (cached.defaultAttrs && attr in cached.defaultAttrs) { - return cached.defaultAttrs[attr]; - } else { - const attrSchema = this.__storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier)[attr]; - const defaultValue = getDefaultValue(attrSchema?.options); - if (typeof attrSchema?.options?.defaultValue === 'function') { - cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record); - cached.defaultAttrs[attr] = defaultValue; + for (let i = 1; i < path.length; i++) { + current = (current as ObjectValue)[path[i]]; + if (current === undefined) { + return undefined; } - return defaultValue; } + return current; } /** @@ -966,29 +1162,114 @@ export default class JSONAPICache implements Cache { * @param field * @param value */ - setAttr(identifier: StableRecordIdentifier, attr: string, value: unknown): void { + setAttr(identifier: StableRecordIdentifier, attr: string | string[], value: Value): void { + // this assert works to ensure we have a non-empty string and/or a non-empty array + assert('setAttr must receive at least one attribute path', attr.length > 0); + const isSimplePath = !Array.isArray(attr) || attr.length === 1; + + if (Array.isArray(attr) && attr.length === 1) { + attr = attr[0]; + } + + if (isSimplePath) { + const cached = this.__peek(identifier, false); + const currentAttr = attr as string; + const existing = + cached.inflightAttrs && currentAttr in cached.inflightAttrs + ? cached.inflightAttrs[currentAttr] + : cached.remoteAttrs && currentAttr in cached.remoteAttrs + ? cached.remoteAttrs[currentAttr] + : undefined; + + if (existing !== value) { + cached.localAttrs = cached.localAttrs || (Object.create(null) as Record); + cached.localAttrs[currentAttr] = value; + cached.changes = cached.changes || (Object.create(null) as Record); + cached.changes[currentAttr] = [existing, value]; + } else if (cached.localAttrs) { + delete cached.localAttrs[currentAttr]; + delete cached.changes![currentAttr]; + } + + if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) { + delete cached.defaultAttrs[currentAttr]; + } + + this._capabilities.notifyChange(identifier, 'attributes', currentAttr); + return; + } + + // get current value from local else inflight else remote + // structuredClone current if not local (or always?) + // traverse path, update value at path + // notify change at first link in path. + // second pass optimization is change notifyChange signature to take an array path + + // guaranteed that we have path of at least 2 in length + const path: string[] = attr as string[]; + const cached = this.__peek(identifier, false); + + // get existing cache record for base path + const basePath = path[0]; const existing = - cached.inflightAttrs && attr in cached.inflightAttrs - ? cached.inflightAttrs[attr] - : cached.remoteAttrs && attr in cached.remoteAttrs - ? cached.remoteAttrs[attr] - : undefined; - if (existing !== value) { - cached.localAttrs = cached.localAttrs || Object.create(null); - cached.localAttrs![attr] = value; - cached.changes = cached.changes || Object.create(null); - cached.changes![attr] = [existing, value]; - } else if (cached.localAttrs) { - delete cached.localAttrs[attr]; - delete cached.changes![attr]; + cached.inflightAttrs && basePath in cached.inflightAttrs + ? cached.inflightAttrs[basePath] + : cached.remoteAttrs && basePath in cached.remoteAttrs + ? cached.remoteAttrs[basePath] + : undefined; + + let existingAttr; + if (existing) { + existingAttr = (existing as ObjectValue)[path[1]]; + + for (let i = 2; i < path.length; i++) { + // the specific change we're making is at path[length - 1] + existingAttr = (existingAttr as ObjectValue)[path[i]]; + } } - if (cached.defaultAttrs && attr in cached.defaultAttrs) { - delete cached.defaultAttrs[attr]; + if (existingAttr !== value) { + cached.localAttrs = cached.localAttrs || (Object.create(null) as Record); + cached.localAttrs[basePath] = cached.localAttrs[basePath] || structuredClone(existing); + cached.changes = cached.changes || (Object.create(null) as Record); + let currentLocal = cached.localAttrs[basePath] as ObjectValue; + let nextLink = 1; + + while (nextLink < path.length - 1) { + currentLocal = currentLocal[path[nextLink++]] as ObjectValue; + } + currentLocal[path[nextLink]] = value as ObjectValue; + + cached.changes[basePath] = [existing, cached.localAttrs[basePath] as ObjectValue]; + + // since we initiaize the value as basePath as a clone of the value at the remote basePath + // then in theory we can use JSON.stringify to compare the two values as key insertion order + // ought to be consistent. + // we try/catch this because users have a habit of doing "Bad Things"TM wherein the cache contains + // stateful values that are not JSON serializable correctly such as Dates. + // in the case that we error, we fallback to not removing the local value + // so that any changes we don't understand are preserved. Thse objects would then sometimes + // appear to be dirty unnecessarily, and for folks that open an issue we can guide them + // to make their cache data less stateful. + } else if (cached.localAttrs) { + try { + if (!existing) { + return; + } + const existingStr = JSON.stringify(existing); + const newStr = JSON.stringify(cached.localAttrs[basePath]); + + if (existingStr !== newStr) { + delete cached.localAttrs[basePath]; + delete cached.changes![basePath]; + } + } catch { + // noop + } } - this.__storeWrapper.notifyChange(identifier, 'attributes', attr); + this._capabilities.notifyChange(identifier, 'attributes', basePath); } /** @@ -996,22 +1277,24 @@ export default class JSONAPICache implements Cache { * * @method changedAttrs * @public - * @deprecated * @param identifier - * @returns { : [, ] } + * @return {ChangedAttributesHash} { : [, ] } */ changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { const cached = this.__peek(identifier, false); - assert(`Cannot retrieve changed attributes for identifier ${identifier} as it is not present in the cache`, cached); + assert( + `Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`, + cached + ); // in Prod we try to recover when accessing something that // doesn't exist if (!cached) { - return Object.create(null); + return Object.create(null) as ChangedAttributesHash; } // TODO freeze in dev - return cached.changes || Object.create(null); + return cached.changes || (Object.create(null) as ChangedAttributesHash); } /** @@ -1024,7 +1307,10 @@ export default class JSONAPICache implements Cache { */ hasChangedAttrs(identifier: StableRecordIdentifier): boolean { const cached = this.__peek(identifier, true); - assert(`Cannot retrieve changed attributes for identifier ${identifier} as it is not present in the cache`, cached); + assert( + `Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`, + cached + ); // in Prod we try to recover when accessing something that // doesn't exist @@ -1046,7 +1332,7 @@ export default class JSONAPICache implements Cache { * @method rollbackAttrs * @public * @param identifier - * @returns {string[]} the names of fields that were restored + * @return {string[]} the names of fields that were restored */ rollbackAttrs(identifier: StableRecordIdentifier): string[] { const cached = this.__peek(identifier, false); @@ -1060,11 +1346,8 @@ export default class JSONAPICache implements Cache { } if (cached.isNew) { - graphFor(this.__storeWrapper).push({ - op: 'deleteRecord', - record: identifier, - isNew: true, - }); + // > Note: Graph removal handled by unloadRecord + cached.isDeletionCommitted = true; cached.isDeleted = true; cached.isNew = false; } @@ -1074,18 +1357,82 @@ export default class JSONAPICache implements Cache { if (cached.errors) { cached.errors = null; - this.__storeWrapper.notifyChange(identifier, 'errors'); + this._capabilities.notifyChange(identifier, 'errors'); } - this.__storeWrapper.notifyChange(identifier, 'state'); + this._capabilities.notifyChange(identifier, 'state'); if (dirtyKeys && dirtyKeys.length) { - notifyAttributes(this.__storeWrapper, identifier, dirtyKeys); + notifyAttributes(this._capabilities, identifier, dirtyKeys); } return dirtyKeys || []; } + /** + * Query the cache for the changes to relationships of a resource. + * + * Returns a map of relationship names to RelationshipDiff objects. + * + * ```ts + * type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; + } + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + ``` + * + * @method changedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {Map} + */ + changedRelationships(identifier: StableRecordIdentifier): Map { + return this.__graph.getChanged(identifier); + } + + /** + * Query the cache for whether any mutated relationships exist + * + * @method hasChangedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {boolean} + */ + hasChangedRelationships(identifier: StableRecordIdentifier): boolean { + return this.__graph.hasChanged(identifier); + } + + /** + * Tell the cache to discard any uncommitted mutations to relationships. + * + * This will also discard the change on any appropriate inverses. + * + * This method is a candidate to become a mutation + * + * @method rollbackRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {string[]} the names of relationships that were restored + */ + rollbackRelationships(identifier: StableRecordIdentifier): string[] { + upgradeCapabilities(this._capabilities); + let result!: string[]; + this._capabilities._store._join(() => { + result = this.__graph.rollback(identifier); + }); + return result; + } + /** * Query the cache for the current state of a relationship property * @@ -1093,13 +1440,10 @@ export default class JSONAPICache implements Cache { * @public * @param identifier * @param field - * @returns resource relationship object + * @return resource relationship object */ - getRelationship( - identifier: StableRecordIdentifier, - field: string - ): SingleResourceRelationship | CollectionResourceRelationship { - return (graphFor(this.__storeWrapper).get(identifier, field) as BelongsToRelationship | ManyRelationship).getData(); + getRelationship(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship { + return this.__graph.getData(identifier, field); } // Resource State @@ -1119,15 +1463,8 @@ export default class JSONAPICache implements Cache { setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { const cached = this.__peek(identifier, false); cached.isDeleted = isDeleted; - if (cached.isNew) { - // TODO can we delete this since we will do this in unload? - graphFor(this.__storeWrapper).push({ - op: 'deleteRecord', - record: identifier, - isNew: true, - }); - } - this.__storeWrapper.notifyChange(identifier, 'state'); + // > Note: Graph removal for isNew handled by unloadRecord + this._capabilities.notifyChange(identifier, 'state'); } /** @@ -1136,9 +1473,9 @@ export default class JSONAPICache implements Cache { * @method getErrors * @public * @param identifier - * @returns {JsonApiError[]} + * @return {JsonApiError[]} */ - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { + getErrors(identifier: StableRecordIdentifier): ApiError[] { return this.__peek(identifier, true).errors || []; } @@ -1148,7 +1485,7 @@ export default class JSONAPICache implements Cache { * @method isEmpty * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isEmpty(identifier: StableRecordIdentifier): boolean { const cached = this.__safePeek(identifier, true); @@ -1162,7 +1499,7 @@ export default class JSONAPICache implements Cache { * @method isNew * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isNew(identifier: StableRecordIdentifier): boolean { // TODO can we assert here? @@ -1176,7 +1513,7 @@ export default class JSONAPICache implements Cache { * @method isDeleted * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isDeleted(identifier: StableRecordIdentifier): boolean { // TODO can we assert here? @@ -1190,7 +1527,7 @@ export default class JSONAPICache implements Cache { * @method isDeletionCommitted * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isDeletionCommitted(identifier: StableRecordIdentifier): boolean { // TODO can we assert here? @@ -1203,7 +1540,7 @@ export default class JSONAPICache implements Cache { * @method _createCache * @internal * @param {StableRecordIdentifier} identifier - * @returns {CachedResource} + * @return {CachedResource} */ _createCache(identifier: StableRecordIdentifier): CachedResource { assert(`Expected no resource data to yet exist in the cache`, !this.__cache.has(identifier)); @@ -1220,7 +1557,7 @@ export default class JSONAPICache implements Cache { * @param {StableRecordIdentifier} identifier * @param {Boolean} allowDestroyed * @internal - * @returns {CachedResource | undefined} + * @return {CachedResource | undefined} */ __safePeek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource | undefined { let resource = this.__cache.get(identifier); @@ -1238,10 +1575,10 @@ export default class JSONAPICache implements Cache { * @param {StableRecordIdentifier} identifier * @param {Boolean} allowDestroyed * @internal - * @returns {CachedResource} + * @return {CachedResource} */ __peek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource { - let resource = this.__safePeek(identifier, allowDestroyed); + const resource = this.__safePeek(identifier, allowDestroyed); assert( `Expected Cache to have a resource entry for the identifier ${String(identifier)} but none was found`, resource @@ -1250,9 +1587,9 @@ export default class JSONAPICache implements Cache { } } -function areAllModelsUnloaded(wrapper: V2CacheStoreWrapper, identifiers: StableRecordIdentifier[]): boolean { +function areAllModelsUnloaded(wrapper: CacheCapabilitiesManager, identifiers: StableRecordIdentifier[]): boolean { for (let i = 0; i < identifiers.length; ++i) { - let identifier = identifiers[i]; + const identifier = identifiers[i]; if (wrapper.hasRecord(identifier)) { return false; } @@ -1260,39 +1597,70 @@ function areAllModelsUnloaded(wrapper: V2CacheStoreWrapper, identifiers: StableR return true; } -function getLocalState(rel) { - if (rel.definition.kind === 'belongsTo') { +function getLocalState(rel: CollectionEdge | ResourceEdge): StableRecordIdentifier[] { + if (isBelongsTo(rel)) { return rel.localState ? [rel.localState] : []; } - return rel.localState; + return rel.additions ? [...rel.additions] : []; } -function getRemoteState(rel) { - if (rel.definition.kind === 'belongsTo') { + +function getRemoteState(rel: CollectionEdge | ResourceEdge) { + if (isBelongsTo(rel)) { return rel.remoteState ? [rel.remoteState] : []; } return rel.remoteState; } -function getDefaultValue(options: { defaultValue?: unknown } | undefined) { - if (!options) { +function schemaHasLegacyDefaultValueFn(schema: FieldSchema | undefined): boolean { + if (!schema) return false; + return hasLegacyDefaultValueFn(schema.options); +} + +function hasLegacyDefaultValueFn(options: object | undefined): options is { defaultValue: () => Value } { + return !!options && typeof (options as { defaultValue: () => Value }).defaultValue === 'function'; +} + +function getDefaultValue( + schema: FieldSchema | undefined, + identifier: StableRecordIdentifier, + store: Store +): Value | undefined { + const options = schema?.options; + + if (!schema || (!options && !schema.type)) { + return; + } + + if (schema.kind !== 'attribute' && schema.kind !== 'field') { return; } - if (typeof options.defaultValue === 'function') { + + // legacy support for defaultValues that are functions + if (hasLegacyDefaultValueFn(options)) { // If anyone opens an issue for args not working right, we'll restore + deprecate it via a Proxy // that lazily instantiates the record. We don't want to provide any args here // because in a non @ember-data/model world they don't make sense. return options.defaultValue(); - } else { - let defaultValue = options.defaultValue; + // legacy support for defaultValues that are primitives + } else if (options && 'defaultValue' in options) { + const defaultValue = options.defaultValue; assert( `Non primitive defaultValues are not supported because they are shared between all instances. If you would like to use a complex object as a default value please provide a function that returns the complex object.`, typeof defaultValue !== 'object' || defaultValue === null ); - return defaultValue; + return defaultValue as Value; + + // new style transforms + } else if (schema.kind !== 'attribute' && schema.type) { + const transform = store.schema.transformation(schema); + + if (transform?.defaultValue) { + return transform.defaultValue(options || null, identifier); + } } } -function notifyAttributes(storeWrapper: CacheStoreWrapper, identifier: StableRecordIdentifier, keys?: string[]) { +function notifyAttributes(storeWrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier, keys?: string[]) { if (!keys) { storeWrapper.notifyChange(identifier, 'attributes'); return; @@ -1308,19 +1676,23 @@ function notifyAttributes(storeWrapper: CacheStoreWrapper, identifier: StableRec There seems to be a potential bug here, where we will return keys that are not in the schema */ -function calculateChangedKeys(cached: CachedResource, updates?: AttributesHash) { - let changedKeys: string[] = []; +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 = Object.assign(Object.create(null), cached.remoteAttrs, cached.inflightAttrs); + const original: Record = Object.assign( + Object.create(null) as Record, + cached.remoteAttrs, + cached.inflightAttrs + ); for (let i = 0; i < length; i++) { - let key = keys[i]; - let value = updates[key]; + 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 @@ -1354,7 +1726,7 @@ function _isEmpty(peeked: CachedResource | undefined): boolean { return (!isNew || isDeleted) && isEmpty; } -function recordIsLoaded(cached: CachedResource | undefined, filterDeleted: boolean = false): boolean { +function recordIsLoaded(cached: CachedResource | undefined, filterDeleted = false): boolean { if (!cached) { return false; } @@ -1377,51 +1749,54 @@ function recordIsLoaded(cached: CachedResource | undefined, filterDeleted: boole function _isLoading( peeked: CachedResource | undefined, - storeWrapper: CacheStoreWrapper, + capabilities: CacheCapabilitiesManager, identifier: StableRecordIdentifier ): boolean { + upgradeCapabilities(capabilities); // TODO refactor things such that the cache is not required to know // about isLoading - // @ts-expect-error - const req = storeWrapper._store.getRequestStateService(); + const req = capabilities._store.getRequestStateService(); // const fulfilled = req.getLastRequestForRecord(identifier); const isLoaded = recordIsLoaded(peeked); return ( !isLoaded && // fulfilled === null && - req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query') + req.getPendingRequestsForRecord(identifier).some((r) => r.type === 'query') ); } function setupRelationships( - storeWrapper: CacheStoreWrapper, + graph: Graph, + capabilities: CacheCapabilitiesManager, identifier: StableRecordIdentifier, - data: JsonApiResource + data: ExistingResourceObject ) { // TODO @runspired iterating by definitions instead of by payload keys // allows relationship payloads to be ignored silently if no relationship // definition exists. Ensure there's a test for this and then consider // moving this to an assertion. This check should possibly live in the graph. - const relationships = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - const keys = Object.keys(relationships); - for (let i = 0; i < keys.length; i++) { - const relationshipName = keys[i]; - const relationshipData = data.relationships![relationshipName]; + const fields = capabilities.schema.fields(identifier); + for (const [name, field] of fields) { + if (!isRelationship(field)) continue; - if (!relationshipData) { - continue; - } + const relationshipData = data.relationships![name]; + if (!relationshipData) continue; - graphFor(storeWrapper).push({ + graph.push({ op: 'updateRelationship', record: identifier, - field: relationshipName, + field: name, value: relationshipData, }); } } +const RelationshipKinds = new Set(['hasMany', 'belongsTo', 'resource', 'collection']); +function isRelationship(field: FieldSchema): field is LegacyRelationshipSchema | CollectionField | ResourceField { + return RelationshipKinds.has(field.kind); +} + function patchLocalAttributes(cached: CachedResource): boolean { const { localAttrs, remoteAttrs, inflightAttrs, defaultAttrs, changes } = cached; if (!localAttrs) { @@ -1429,16 +1804,16 @@ function patchLocalAttributes(cached: CachedResource): boolean { return false; } let hasAppliedPatch = false; - let mutatedKeys = Object.keys(localAttrs); + const mutatedKeys = Object.keys(localAttrs); for (let i = 0, length = mutatedKeys.length; i < length; i++) { - let attr = mutatedKeys[i]; + const attr = mutatedKeys[i]; const existing = inflightAttrs && attr in inflightAttrs ? inflightAttrs[attr] : remoteAttrs && attr in remoteAttrs - ? remoteAttrs[attr] - : undefined; + ? remoteAttrs[attr] + : undefined; if (existing === localAttrs[attr]) { hasAppliedPatch = true; @@ -1458,6 +1833,14 @@ function putOne( identifiers: IdentifierCache, resource: ExistingResourceObject ): StableExistingRecordIdentifier { + assert( + `You must include an 'id' for the resource data ${resource.type}`, + resource.id !== null && resource.id !== undefined && resource.id !== '' + ); + assert( + `Missing Resource Type: received resource data with a type '${resource.type}' but no schema could be found with that name.`, + cache._capabilities.schema.hasResource(resource) + ); let identifier: StableRecordIdentifier | undefined = identifiers.peekRecordIdentifier(resource); if (identifier) { @@ -1465,7 +1848,7 @@ function putOne( } else { identifier = identifiers.getOrCreateRecordIdentifier(resource); } - cache.upsert(identifier, resource, cache.__storeWrapper.hasRecord(identifier)); + cache.upsert(identifier, resource, cache._capabilities.hasRecord(identifier)); // even if the identifier was not "existing" before, it is now return identifier as StableExistingRecordIdentifier; } @@ -1474,7 +1857,10 @@ function putOne( Iterates over the set of internal models reachable from `this` across exactly one relationship. */ -function _directlyRelatedIdentifiersIterable(storeWrapper: CacheStoreWrapper, originating: StableRecordIdentifier) { +function _directlyRelatedIdentifiersIterable( + storeWrapper: CacheCapabilitiesManager, + originating: StableRecordIdentifier +) { const graph = peekGraph(storeWrapper); const initializedRelationships = graph?.identifiers.get(originating); @@ -1482,7 +1868,7 @@ function _directlyRelatedIdentifiersIterable(storeWrapper: CacheStoreWrapper, or return EMPTY_ITERATOR; } - const initializedRelationshipsArr: Array = []; + const initializedRelationshipsArr: Array = []; Object.keys(initializedRelationships).forEach((key) => { const rel = initializedRelationships[key]; if (rel && !isImplicit(rel)) { @@ -1494,13 +1880,13 @@ function _directlyRelatedIdentifiersIterable(storeWrapper: CacheStoreWrapper, or let j = 0; let k = 0; - const findNext = () => { + const findNext = (): StableRecordIdentifier | undefined => { while (i < initializedRelationshipsArr.length) { while (j < 2) { - let relatedIdentifiers = + const relatedIdentifiers = j === 0 ? getLocalState(initializedRelationshipsArr[i]) : getRemoteState(initializedRelationshipsArr[i]); while (k < relatedIdentifiers.length) { - let relatedIdentifier = relatedIdentifiers[k++]; + const relatedIdentifier = relatedIdentifiers[k++]; if (relatedIdentifier !== null) { return relatedIdentifier; } @@ -1517,7 +1903,7 @@ function _directlyRelatedIdentifiersIterable(storeWrapper: CacheStoreWrapper, or return { iterator() { return { - next: () => { + next: (): { value: StableRecordIdentifier | undefined; done: boolean } => { const value = findNext(); return { value, done: value === undefined }; }, @@ -1537,24 +1923,24 @@ function _directlyRelatedIdentifiersIterable(storeWrapper: CacheStoreWrapper, or from `this.identifier`. */ function _allRelatedIdentifiers( - storeWrapper: CacheStoreWrapper, + storeWrapper: CacheCapabilitiesManager, originating: StableRecordIdentifier ): StableRecordIdentifier[] { - let array: StableRecordIdentifier[] = []; - let queue: StableRecordIdentifier[] = []; - let seen = new Set(); + const array: StableRecordIdentifier[] = []; + const queue: StableRecordIdentifier[] = []; + const seen = new Set(); queue.push(originating); while (queue.length > 0) { - let identifier = queue.shift()!; + const identifier = queue.shift()!; array.push(identifier); seen.add(identifier); const iterator = _directlyRelatedIdentifiersIterable(storeWrapper, originating).iterator(); for (let obj = iterator.next(); !obj.done; obj = iterator.next()) { - const identifier = obj.value; - if (identifier && !seen.has(identifier)) { - seen.add(identifier); - queue.push(identifier); + const relatedIdentifier = obj.value; + if (relatedIdentifier && !seen.has(relatedIdentifier)) { + seen.add(relatedIdentifier); + queue.push(relatedIdentifier); } } } @@ -1565,7 +1951,13 @@ function _allRelatedIdentifiers( function isMetaDocument( doc: StructuredDocument ): doc is StructuredDataDocument { - return !(doc instanceof Error) && !('data' in doc.content) && !('included' in doc.content) && 'meta' in doc.content; + return ( + !(doc instanceof Error) && + doc.content && + !('data' in doc.content) && + !('included' in doc.content) && + 'meta' in doc.content + ); } function isErrorDocument( diff --git a/packages/json-api/src/-private/serialize.ts b/packages/json-api/src/-private/serialize.ts new file mode 100644 index 00000000000..7cfb58ac95e --- /dev/null +++ b/packages/json-api/src/-private/serialize.ts @@ -0,0 +1,126 @@ +/** + * @module @ember-data/json-api/request + */ +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'; +import type { Relationship } from '@warp-drive/core-types/cache/relationship'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { ResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; + +type ChangedRelationshipData = { + data: Relationship['data']; +}; + +export type JsonApiResourcePatch = { + type: string; + id: string | null; + lid: string; + attributes?: Record; + relationships?: Record; +}; + +/** + * Serializes the current state of a resource or array of resources for use with POST or PUT requests. + * + * @method serializeResources + * @static + * @public + * @for @ember-data/json-api/request + * @param {Cache} cache} + * @param {StableRecordIdentifier} identifier + * @return {object} An object with a `data` property containing the serialized resource patch + */ +export function serializeResources(cache: Cache, identifiers: StableRecordIdentifier): { data: ResourceObject }; +export function serializeResources(cache: Cache, identifiers: StableRecordIdentifier[]): { data: ResourceObject[] }; +export function serializeResources( + cache: Cache, + identifiers: StableRecordIdentifier | StableRecordIdentifier[] +): { data: ResourceObject | ResourceObject[] } { + return { + data: Array.isArray(identifiers) + ? identifiers.map((identifier) => _serializeResource(cache, identifier)) + : _serializeResource(cache, identifiers), + }; +} + +function _serializeResource(cache: Cache, identifier: StableRecordIdentifier): ResourceObject { + const { id, lid, type } = identifier; + // yup! this method actually does nothing. It's just here for the dev assertion + // and to assist in providing a little sugar to the consuming app via the `serializeResources` utility + const record = cache.peek(identifier) as ResourceObject; + assert( + `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`, + record + ); + + return record; +} + +/** + * Serializes changes to a resource for use with PATCH requests. + * + * Only attributes which are changed are serialized. + * Only relationships which are changed are serialized. + * + * Collection relationships serialize the collection as a whole. + * + * If you would like to serialize updates to a collection more granularly + * (for instance, as operations) request the diff from the store and + * serialize as desired: + * + * ```ts + * const relationshipDiffMap = cache.changedRelationships(identifier); + * ``` + * + * @method serializePatch + * @static + * @public + * @for @ember-data/json-api/request + * @param {Cache} cache} + * @param {StableRecordIdentifier} identifier + * @return {object} An object with a `data` property containing the serialized resource patch + */ +export function serializePatch( + cache: Cache, + identifier: StableRecordIdentifier + // options: { include?: string[] } = {} +): { data: JsonApiResourcePatch } { + const { id, lid, type } = identifier; + const record = cache.peek(identifier) as ResourceObject; + assert( + `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`, + record + ); + + const data: JsonApiResourcePatch = { + type, + lid, + id, + }; + + if (cache.hasChangedAttrs(identifier)) { + const attrsChanges = cache.changedAttrs(identifier); + const attributes: ResourceObject['attributes'] = {}; + + Object.keys(attrsChanges).forEach((key) => { + const change = attrsChanges[key]; + const newVal = change[1]; + attributes[key] = newVal === undefined ? null : newVal; + }); + + data.attributes = attributes; + } + + const changedRelationships = cache.changedRelationships(identifier); + if (changedRelationships.size) { + const relationships: Record = {}; + changedRelationships.forEach((diff, key) => { + relationships[key] = { data: diff.localState }; + }); + + data.relationships = relationships; + } + + return { data }; +} diff --git a/packages/json-api/src/request.ts b/packages/json-api/src/request.ts new file mode 100644 index 00000000000..b890789ab9d --- /dev/null +++ b/packages/json-api/src/request.ts @@ -0,0 +1,61 @@ +/** + *

+ +

+ +This package provides utilities for working with [JSON:API](https://json-api.org) APIs with [*Ember***Data**](https://github.com/emberjs/data/). + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/json-api +``` + +## Usage + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/json-api/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/* + { + url: 'https://api.example.com/v1/ember-developers/1?include=friends,pets', + method: 'GET', + headers: , + // => 'Accept': 'application/vnd.api+json' + // => 'Content-Type': 'application/vnd.api+json' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +*\ +``` + +Request builder output may be used with either `requestManager.request` or `store.request`. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common JSON:API format (dasherized pluralized resource types). + * + * @module @ember-data/json-api/request + * @main @ember-data/json-api/request + */ +export { findRecord } from './-private/builders/find-record'; +export { query, postQuery } from './-private/builders/query'; +export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record'; +export { serializeResources, serializePatch } from './-private/serialize'; +export { setBuildURLConfig } from './-private/builders/-utils'; diff --git a/packages/json-api/tsconfig.json b/packages/json-api/tsconfig.json new file mode 100644 index 00000000000..d80220df145 --- /dev/null +++ b/packages/json-api/tsconfig.json @@ -0,0 +1,71 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/graph": ["../graph/unstable-preview-types"], + "@ember-data/graph/*": ["../graph/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../graph" + }, + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/json-api/vite.config.mjs b/packages/json-api/vite.config.mjs new file mode 100644 index 00000000000..792950e9638 --- /dev/null +++ b/packages/json-api/vite.config.mjs @@ -0,0 +1,13 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = []; + +export const entryPoints = ['./src/index.ts', './src/request.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/legacy-compat/CHANGELOG.md b/packages/legacy-compat/CHANGELOG.md new file mode 100644 index 00000000000..37b34f39e6d --- /dev/null +++ b/packages/legacy-compat/CHANGELOG.md @@ -0,0 +1,87 @@ +# @ember-data/legacy-compat Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9474](https://github.com/emberjs/data/pull/9474) Improve query types for legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) +* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) +* [#9400](https://github.com/emberjs/data/pull/9400) feat: add expectId util ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#9319](https://github.com/emberjs/data/pull/9319) Add @ember-data/legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) +* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#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)) +* [#9279](https://github.com/emberjs/data/pull/9279) types: branded transforms and improve types needed for serializers ([@runspired](https://github.com/runspired)) + +#### Committers: (4) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) +Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) +* [#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)) +* [#8935](https://github.com/emberjs/data/pull/8935) feat: (private) implement basic field support for schema-record ([@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 + +* [#8934](https://github.com/emberjs/data/pull/8934) fix: JSONAPISerializer should not reify empty records ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#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)) +* [#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)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@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: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/legacy-compat/README.md b/packages/legacy-compat/README.md index a5e19072e51..a0eb1acbf22 100644 --- a/packages/legacy-compat/README.md +++ b/packages/legacy-compat/README.md @@ -24,3 +24,11 @@ Install using your javascript package manager of choice. For instance with [pnpm ```no-highlight pnpm add @ember-data/legacy-compat ``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/legacy-compat/lts-4-12?label=%40lts-4-12&color=bbbbbb) diff --git a/packages/legacy-compat/addon-main.cjs b/packages/legacy-compat/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/legacy-compat/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/legacy-compat/addon-main.js b/packages/legacy-compat/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/legacy-compat/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/legacy-compat/babel.config.js b/packages/legacy-compat/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/legacy-compat/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/legacy-compat/babel.config.mjs b/packages/legacy-compat/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/legacy-compat/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/legacy-compat/eslint.config.mjs b/packages/legacy-compat/eslint.config.mjs new file mode 100644 index 00000000000..f534f43cf95 --- /dev/null +++ b/packages/legacy-compat/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: ['@ember/debug', '@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/legacy-compat/package.json b/packages/legacy-compat/package.json index 0177c498d74..c6f0de193e9 100644 --- a/packages/legacy-compat/package.json +++ b/packages/legacy-compat/package.json @@ -12,7 +12,7 @@ "homepage": "https://github.com/emberjs/data", "bugs": "https://github.com/emberjs/data/issues", "engines": { - "node": "16.* || >= 18" + "node": ">= 18.20.4" }, "keywords": [ "ember-addon" @@ -20,43 +20,62 @@ "volta": { "extends": "../../package.json" }, - "dependencies": { - "ember-cli-babel": "^7.26.11", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0" - }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@ember-data/request": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/store": { "injected": true }, - "@ember/string": { + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, - "peerDependencies": { - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0" - }, "peerDependenciesMeta": { "@ember-data/graph": { "optional": true @@ -65,26 +84,39 @@ "optional": true } }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember/test-waiters": "^3.1.0", + "@warp-drive/core-types": "workspace:*" + }, "devDependencies": { - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember/string": "^4.0.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "@types/ember__string": "^3.0.15", - "tslib": "^2.5.0", - "walk-sync": "^3.0.0", - "typescript": "^5.0.3" + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/test-waiters": "^3.1.0", + "@glimmer/component": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "ember": { "edition": "octane" diff --git a/packages/legacy-compat/rollup.config.mjs b/packages/legacy-compat/rollup.config.mjs deleted file mode 100644 index e15a77f21c0..00000000000 --- a/packages/legacy-compat/rollup.config.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: ['@embroider/macros', '@ember-data/store/-private'], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'builders.js', '-private.js']), - - nodeResolve({ extensions: ['.ts'] }), - babel({ - extensions: ['.ts'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/legacy-compat/src/-private.ts b/packages/legacy-compat/src/-private.ts index 0fccb41362a..015db6e1ba3 100644 --- a/packages/legacy-compat/src/-private.ts +++ b/packages/legacy-compat/src/-private.ts @@ -1,4 +1,17 @@ -export { default as SnapshotRecordArray } from './legacy-network-handler/snapshot-record-array'; +import type Store from '@ember-data/store'; + +import type { CompatStore } from '.'; + +/** + * Utilities - often temporary - for maintaining backwards compatibility with + * older parts of EmberData. + * + @module @ember-data/legacy-compat + @main @ember-data/legacy-compat +*/ +export { SnapshotRecordArray } from './legacy-network-handler/snapshot-record-array'; export { SaveOp } from './legacy-network-handler/fetch-manager'; -export { default as FetchManager } from './legacy-network-handler/fetch-manager'; -export { default as Snapshot } from './legacy-network-handler/snapshot'; +export { FetchManager } from './legacy-network-handler/fetch-manager'; +export { Snapshot } from './legacy-network-handler/snapshot'; + +export function upgradeStore(store: Store): asserts store is CompatStore {} diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index 5b42e847ce4..5f5f457c4ad 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -1,8 +1,10 @@ /** Builders for migrating from `store` methods to `store.request`. + These builders enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. To that end, these builders are deprecated and will be removed in a future version of Ember Data. + @module @ember-data/legacy-compat/builders @main @ember-data/legacy-compat/builders @deprecated diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index e449412f73c..08e771f1783 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -1,28 +1,25 @@ /** * @module @ember-data/legacy-compat/builders */ -import { assert } from '@ember/debug'; - -import { SkipCache } from '@ember-data/request'; -import type { ImmutableRequestInfo } from '@ember-data/request/-private/types'; +import type { StoreRequestInput } from '@ember-data/store'; +import type { FindAllOptions } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { normalizeModelName } from './utils'; -// Keeping unused generics for consistency with 5x types -type FindAllRequestInput = ImmutableRequestInfo & { +type FindAllRequestInput = StoreRequestInput & { op: 'findAll'; data: { type: T; options: FindAllBuilderOptions; }; + [RequestSignature]?: RT; }; -type FindAllBuilderOptions = { - reload?: boolean; - backgroundReload?: boolean; - include?: string | string[]; - adapterOptions?: Record; -}; +type FindAllBuilderOptions = FindAllOptions; /** This function builds a request config to perform a `findAll` request for the given type. @@ -43,7 +40,10 @@ type FindAllBuilderOptions = { @param {FindAllBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.findAll @return {FindAllRequestInput} request config */ -export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; +export function findAllBuilder( + type: TypeFromInstance, + options?: FindAllBuilderOptions +): FindAllRequestInput, T[]>; export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; export function findAllBuilder(type: string, options: FindAllBuilderOptions = {}): FindAllRequestInput { assert(`You need to pass a model name to the findAll builder`, type); @@ -58,6 +58,6 @@ export function findAllBuilder(type: string, options: FindAllBuilderOptions = {} type: normalizeModelName(type), options: options || {}, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }; } diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 33e61d666a2..580f5df8b81 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -1,29 +1,27 @@ /** * @module @ember-data/legacy-compat/builders */ -import { assert } from '@ember/debug'; - -import { SkipCache } from '@ember-data/request'; -import type { ImmutableRequestInfo } from '@ember-data/request/-private/types'; +import type { StoreRequestInput } from '@ember-data/store'; import { constructResource, ensureStringId } from '@ember-data/store/-private'; -import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; +import type { BaseFinderOptions, FindRecordOptions } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; +import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { isMaybeIdentifier, normalizeModelName } from './utils'; -type FindRecordRequestInput = ImmutableRequestInfo & { +type FindRecordRequestInput = StoreRequestInput & { op: 'findRecord'; data: { - record: ResourceIdentifierObject; + record: ResourceIdentifierObject; options: FindRecordBuilderOptions; }; + [RequestSignature]?: RT; }; -type FindRecordBuilderOptions = { - reload?: boolean; - backgroundReload?: boolean; - include?: string | string[]; - adapterOptions?: Record; -}; +type FindRecordBuilderOptions = Omit; /** This function builds a request config to find the record for a given identifier or type and id combination. @@ -57,25 +55,21 @@ type FindRecordBuilderOptions = { @public @static @for @ember-data/legacy-compat/builders - @param {string|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record + @param {string|object} resource - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record @param {string|number|object} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved @param {FindRecordBuilderOptions} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. @return {FindRecordRequestInput} request config */ -export function findRecordBuilder( - resource: string, +export function findRecordBuilder( + type: TypeFromInstance, id: string, options?: FindRecordBuilderOptions -): FindRecordRequestInput; -export function findRecordBuilder( - resource: string, - id: string, +): FindRecordRequestInput, T>; +export function findRecordBuilder(type: string, id: string, options?: FindRecordBuilderOptions): FindRecordRequestInput; +export function findRecordBuilder( + resource: ResourceIdentifierObject>, options?: FindRecordBuilderOptions -): FindRecordRequestInput; -export function findRecordBuilder( - resource: ResourceIdentifierObject, - options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput, T>; export function findRecordBuilder( resource: ResourceIdentifierObject, options?: FindRecordBuilderOptions @@ -90,7 +84,7 @@ export function findRecordBuilder( resource ); if (isMaybeIdentifier(resource)) { - options = idOrOptions as FindRecordBuilderOptions | undefined; + options = idOrOptions as BaseFinderOptions | undefined; } else { assert( `You need to pass a modelName or resource identifier as the first argument to the findRecord builder (passed ${resource})`, @@ -103,7 +97,7 @@ export function findRecordBuilder( options = options || {}; - assert('findRecord builder does not support options.preload', !(options as { preload?: boolean }).preload); + assert('findRecord builder does not support options.preload', !(options as FindRecordOptions).preload); return { op: 'findRecord' as const, @@ -111,6 +105,6 @@ export function findRecordBuilder( record: resource, options, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }; } diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index 5d7dbe63bcc..484570c199a 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -1,25 +1,26 @@ /** * @module @ember-data/legacy-compat/builders */ -import { assert } from '@ember/debug'; - -import { SkipCache } from '@ember-data/request'; -import type { ImmutableRequestInfo } from '@ember-data/request/-private/types'; +import type { StoreRequestInput } from '@ember-data/store'; +import type { LegacyResourceQuery, QueryOptions } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { normalizeModelName } from './utils'; -type QueryRequestInput = ImmutableRequestInfo & { +type QueryRequestInput = StoreRequestInput & { op: 'query'; data: { type: T; - query: Record; + query: LegacyResourceQuery; options: QueryBuilderOptions; }; + [RequestSignature]?: RT; }; -type QueryBuilderOptions = { - [K in string | 'adapterOptions']?: K extends 'adapterOptions' ? Record : unknown; -}; +type QueryBuilderOptions = QueryOptions; /** This function builds a request config for a given type and query object. @@ -40,19 +41,19 @@ type QueryBuilderOptions = { @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query @return {QueryRequestInput} request config */ -export function queryBuilder( - type: string, - query: Record, +export function queryBuilder( + type: TypeFromInstance, + query: LegacyResourceQuery, options?: QueryBuilderOptions -): QueryRequestInput; +): QueryRequestInput, T[]>; export function queryBuilder( type: string, - query: Record, + query: LegacyResourceQuery, options?: QueryBuilderOptions ): QueryRequestInput; export function queryBuilder( type: string, - query: Record, + query: LegacyResourceQuery, options: QueryBuilderOptions = {} ): QueryRequestInput { assert(`You need to pass a model name to the query builder`, type); @@ -69,17 +70,18 @@ export function queryBuilder( query, options: options, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }; } -type QueryRecordRequestInput = ImmutableRequestInfo & { +type QueryRecordRequestInput = StoreRequestInput & { op: 'queryRecord'; data: { type: T; - query: Record; + query: LegacyResourceQuery; options: QueryBuilderOptions; }; + [RequestSignature]?: RT; }; /** @@ -101,19 +103,19 @@ type QueryRecordRequestInput = ImmutableRequestInfo & @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query @return {QueryRecordRequestInput} request config */ -export function queryRecordBuilder( - type: string, - query: Record, +export function queryRecordBuilder( + type: TypeFromInstance, + query: LegacyResourceQuery, options?: QueryBuilderOptions -): QueryRecordRequestInput; +): QueryRecordRequestInput, T | null>; export function queryRecordBuilder( type: string, - query: Record, + query: LegacyResourceQuery, options?: QueryBuilderOptions ): QueryRecordRequestInput; export function queryRecordBuilder( type: string, - query: Record, + query: LegacyResourceQuery, options?: QueryBuilderOptions ): QueryRecordRequestInput { assert(`You need to pass a model name to the queryRecord builder`, type); @@ -130,6 +132,6 @@ export function queryRecordBuilder( query, options: options || {}, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }; } diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index faa9bc93027..3aaae565901 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -1,23 +1,23 @@ /** * @module @ember-data/legacy-compat/builders */ -import { assert } from '@ember/debug'; +import { recordIdentifierFor, storeFor, type StoreRequestInput } from '@ember-data/store'; +import type { InstanceCache } from '@ember-data/store/-private'; +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'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; -import type Model from '@ember-data/model'; -import { SkipCache } from '@ember-data/request'; -import type { ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import { recordIdentifierFor, storeFor } from '@ember-data/store'; -import type { InstanceCache } from '@ember-data/store/-private/caches/instance-cache'; -import type { Cache } from '@ember-data/types/cache/cache'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -type SaveRecordRequestInput = ImmutableRequestInfo & { +type SaveRecordRequestInput = StoreRequestInput & { op: 'createRecord' | 'deleteRecord' | 'updateRecord'; data: { - record: StableRecordIdentifier; + record: StableRecordIdentifier; options: SaveRecordBuilderOptions; }; - records: [StableRecordIdentifier]; + records: [StableRecordIdentifier]; + [RequestSignature]?: RT; }; type SaveRecordBuilderOptions = Record; @@ -49,13 +49,13 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable @param {SaveRecordBuilderOptions} options optional, may include `adapterOptions` hash which will be passed to adapter.saveRecord @return {SaveRecordRequestInput} request config */ -export function saveRecordBuilder( +export function saveRecordBuilder( record: T, options: Record = {} -): SaveRecordRequestInput { +): SaveRecordRequestInput, T> { const store = storeFor(record); assert(`Unable to initiate save for a record in a disconnected state`, store); - const identifier = recordIdentifierFor(record); + const identifier = recordIdentifierFor(record); if (!identifier) { // this commonly means we're disconnected @@ -89,6 +89,6 @@ export function saveRecordBuilder( record: identifier, }, records: [identifier], - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }; } diff --git a/packages/legacy-compat/src/builders/utils.ts b/packages/legacy-compat/src/builders/utils.ts index d6c04f18c73..dc29dd7bb26 100644 --- a/packages/legacy-compat/src/builders/utils.ts +++ b/packages/legacy-compat/src/builders/utils.ts @@ -1,6 +1,8 @@ -import { dasherize } from '@ember/string'; +import { deprecate } from '@ember/debug'; -import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; +import { dasherize } from '@ember-data/request-utils/string'; +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( maybeIdentifier: string | ResourceIdentifierObject @@ -14,5 +16,25 @@ export function isMaybeIdentifier( } export function normalizeModelName(type: string): string { - return dasherize(type); + if (DEPRECATE_NON_STRICT_TYPES) { + const result = 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; } diff --git a/packages/legacy-compat/src/index.ts b/packages/legacy-compat/src/index.ts index c7a386f22ca..c9077b86e46 100644 --- a/packages/legacy-compat/src/index.ts +++ b/packages/legacy-compat/src/index.ts @@ -1,8 +1,329 @@ -/** - * Utilities - often temporary - for maintaining backwards compatibility with - * older parts of EmberData. - * - @module @ember-data/legacy-compat - @main @ember-data/legacy-compat -*/ +import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; + +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +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, + SerializerOptions, +} from './legacy-network-handler/minimum-serializer-interface'; + export { LegacyNetworkHandler } from './legacy-network-handler/legacy-network-handler'; + +export type { MinimumAdapterInterface, MinimumSerializerInterface, SerializerOptions, AdapterPayload }; + +/** + * @module @ember-data/store + * @class Store + */ +export type LegacyStoreCompat = { + _fetchManager: FetchManager; + adapterFor(this: Store, modelName: string): MinimumAdapterInterface; + adapterFor(this: Store, modelName: string, _allowMissing: true): MinimumAdapterInterface | undefined; + + serializerFor(modelName: K, _allowMissing?: boolean): MinimumSerializerInterface | null; + + normalize(modelName: string, payload: ObjectValue): ObjectValue; + pushPayload(modelName: string, payload: ObjectValue): void; + serializeRecord(record: unknown, options?: SerializerOptions): unknown; + + _adapterCache: Record; + _serializerCache: Record; +}; + +export type CompatStore = Store & LegacyStoreCompat; + +/** + Returns an instance of the adapter for a given type. For + example, `adapterFor('person')` will return an instance of + the adapter located at `app/adapters/person.js` + + If no `person` adapter is found, this method will look + for an `application` adapter (the default adapter for + your entire application). + + @method adapterFor + @public + @param {String} modelName + @return Adapter + */ +export function adapterFor(this: Store, modelName: string): MinimumAdapterInterface; +export function adapterFor(this: Store, modelName: string, _allowMissing: true): MinimumAdapterInterface | undefined; +export function adapterFor(this: Store, modelName: string, _allowMissing?: true): MinimumAdapterInterface | undefined { + assert( + `Attempted to call store.adapterFor(), but the store instance has already been destroyed.`, + !(this.isDestroying || this.isDestroyed) + ); + assert(`You need to pass a model name to the store's adapterFor method`, modelName); + assert( + `Passing classes to store.adapterFor has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + upgradeStore(this); + this._adapterCache = + this._adapterCache || (Object.create(null) as Record); + + const normalizedModelName = normalizeModelName(modelName); + + const { _adapterCache } = this; + let adapter: (MinimumAdapterInterface & { store: Store }) | undefined = _adapterCache[normalizedModelName]; + if (adapter) { + return adapter; + } + + const owner = getOwner(this)!; + + // name specific adapter + adapter = owner.lookup(`adapter:${normalizedModelName}`) as (MinimumAdapterInterface & { store: Store }) | undefined; + if (adapter !== undefined) { + _adapterCache[normalizedModelName] = adapter; + return adapter; + } + + // no adapter found for the specific name, fallback and check for application adapter + adapter = _adapterCache.application || owner.lookup('adapter:application'); + if (adapter !== undefined) { + _adapterCache[normalizedModelName] = adapter; + _adapterCache.application = adapter; + 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 + ); +} + +/** + Returns an instance of the serializer for a given type. For + example, `serializerFor('person')` will return an instance of + `App.PersonSerializer`. + + If no `App.PersonSerializer` is found, this method will look + for an `App.ApplicationSerializer` (the default serializer for + your entire application). + + If a serializer cannot be found on the adapter, it will fall back + to an instance of `JSONSerializer`. + + @method serializerFor + @public + @param {String} modelName the record to serialize + @return {Serializer} + */ +export function serializerFor(this: Store, modelName: string): MinimumSerializerInterface | null { + assert( + `Attempted to call store.serializerFor(), but the store instance has already been destroyed.`, + !(this.isDestroying || this.isDestroyed) + ); + assert(`You need to pass a model name to the store's serializerFor method`, modelName); + assert( + `Passing classes to store.serializerFor has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + upgradeStore(this); + this._serializerCache = + this._serializerCache || (Object.create(null) as Record); + const normalizedModelName = normalizeModelName(modelName); + + const { _serializerCache } = this; + let serializer: (MinimumSerializerInterface & { store: Store }) | undefined = _serializerCache[normalizedModelName]; + if (serializer) { + return serializer; + } + + // by name + const owner = getOwner(this)!; + serializer = owner.lookup(`serializer:${normalizedModelName}`) as + | (MinimumSerializerInterface & { store: Store }) + | undefined; + if (serializer !== undefined) { + _serializerCache[normalizedModelName] = serializer; + return serializer; + } + + // no serializer found for the specific model, fallback and check for application serializer + serializer = _serializerCache.application || owner.lookup('serializer:application'); + if (serializer !== undefined) { + _serializerCache[normalizedModelName] = serializer; + _serializerCache.application = serializer; + return serializer; + } + + return null; +} + +/** + `normalize` converts a json payload into the normalized form that + [push](../methods/push?anchor=push) expects. + + Example + + ```js + socket.on('message', function(message) { + let modelName = message.model; + let data = message.data; + store.push(store.normalize(modelName, data)); + }); + ``` + + @method normalize + @public + @param {String} modelName The name of the model type for this payload + @param {Object} payload + @return {Object} The normalized payload + */ +// TODO @runspired @deprecate users should call normalize on the associated serializer directly +export function normalize(this: Store, modelName: string, payload: ObjectValue) { + upgradeStore(this); + assert( + `Attempted to call store.normalize(), but the store instance has already been destroyed.`, + !(this.isDestroying || this.isDestroyed) + ); + assert(`You need to pass a model name to the store's normalize method`, modelName); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, + typeof modelName === 'string' + ); + const normalizedModelName = normalizeModelName(modelName); + const serializer = this.serializerFor(normalizedModelName); + const schema = this.modelFor(normalizedModelName); + assert( + `You must define a normalize method in your serializer in order to call store.normalize`, + typeof serializer?.normalize === 'function' + ); + return serializer.normalize(schema, payload); +} + +/** + Push some raw data into the store. + + This method can be used both to push in brand new + records, as well as to update existing records. You + can push in more than one type of object at once. + All objects should be in the format expected by the + serializer. + + ```app/serializers/application.js + import RESTSerializer from '@ember-data/serializer/rest'; + + export default class ApplicationSerializer extends RESTSerializer; + ``` + + ```js + let pushData = { + posts: [ + { id: 1, postTitle: "Great post", commentIds: [2] } + ], + comments: [ + { id: 2, commentBody: "Insightful comment" } + ] + } + + store.pushPayload(pushData); + ``` + + By default, the data will be deserialized using a default + serializer (the application serializer if it exists). + + Alternatively, `pushPayload` will accept a model type which + will determine which serializer will process the payload. + + ```app/serializers/application.js + import RESTSerializer from '@ember-data/serializer/rest'; + + export default class ApplicationSerializer extends RESTSerializer; + ``` + + ```app/serializers/post.js + import JSONSerializer from '@ember-data/serializer/json'; + + export default JSONSerializer; + ``` + + ```js + store.pushPayload(pushData); // Will use the application serializer + store.pushPayload('post', pushData); // Will use the post serializer + ``` + + @method pushPayload + @public + @param {String} modelName Optionally, a model type used to determine which serializer will be used + @param {Object} inputPayload + */ +// TODO @runspired @deprecate pushPayload in favor of looking up the serializer +export function pushPayload(this: Store, modelName: string, inputPayload: ObjectValue): void { + upgradeStore(this); + assert( + `Attempted to call store.pushPayload(), but the store instance has already been destroyed.`, + !(this.isDestroying || this.isDestroyed) + ); + + const payload: ObjectValue = inputPayload || (modelName as unknown as ObjectValue); + const normalizedModelName = inputPayload ? normalizeModelName(modelName) : 'application'; + const serializer = this.serializerFor(normalizedModelName); + + assert( + `You cannot use 'store.pushPayload(, )' unless the serializer for '${normalizedModelName}' defines 'pushPayload'`, + serializer && typeof serializer.pushPayload === 'function' + ); + serializer.pushPayload(this, payload); +} + +// TODO @runspired @deprecate records should implement their own serialization if desired +export function serializeRecord(this: Store, record: unknown, options?: SerializerOptions): unknown { + upgradeStore(this); + // TODO we used to check if the record was destroyed here + if (!this._fetchManager) { + this._fetchManager = new FetchManager(this); + } + + return this._fetchManager.createSnapshot(recordIdentifierFor(record)).serialize(options); +} + +export function cleanup(this: Store) { + upgradeStore(this); + // enqueue destruction of any adapters/serializers we have created + for (const adapterName in this._adapterCache) { + const adapter = this._adapterCache[adapterName]; + if (typeof adapter.destroy === 'function') { + adapter.destroy(); + } + } + + for (const serializerName in this._serializerCache) { + const serializer = this._serializerCache[serializerName]; + if (typeof serializer.destroy === 'function') { + serializer.destroy(); + } + } +} diff --git a/packages/legacy-compat/src/legacy-network-handler/common.js b/packages/legacy-compat/src/legacy-network-handler/common.js deleted file mode 100644 index 43d3db72d4f..00000000000 --- a/packages/legacy-compat/src/legacy-network-handler/common.js +++ /dev/null @@ -1,47 +0,0 @@ -import { deprecate } from '@ember/debug'; - -import { DEPRECATE_RSVP_PROMISE } from '@ember-data/deprecations'; - -export function _bind(fn, ...args) { - return function () { - return fn.apply(undefined, args); - }; -} - -export function _guard(promise, test) { - let guarded = promise.finally(() => { - if (!test()) { - guarded._subscribers ? (guarded._subscribers.length = 0) : null; - } - }); - - return guarded; -} - -export function _objectIsAlive(object) { - return !(object.isDestroyed || object.isDestroying); -} - -export function guardDestroyedStore(promise, store) { - 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; - }); -} 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 72d557ee378..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,49 +1,52 @@ -import { assert, deprecate, warn } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; -import { importSync } from '@embroider/macros'; +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; -import { DEPRECATE_RSVP_PROMISE, DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG, TESTING } from '@ember-data/env'; -import { HAS_GRAPH_PACKAGE } from '@ember-data/packages'; import { createDeferred } from '@ember-data/request'; -import type { Deferred, ImmutableRequestInfo } from '@ember-data/request/-private/types'; import type Store from '@ember-data/store'; -import { coerceId } from '@ember-data/store/-private'; -import type { InstanceCache } from '@ember-data/store/-private/caches/instance-cache'; -import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import type RequestStateService from '@ember-data/store/-private/network/request-cache'; -import type { CollectionResourceDocument, SingleResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { FindRecordQuery, Request, SaveRecordMutation } from '@ember-data/types/q/fetch-manager'; import type { - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import { AdapterPayload, MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; -import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; -import type { FindOptions } from '@ember-data/types/q/store'; - -import { _objectIsAlive, guardDestroyedStore } from './common'; + FindRecordQuery, + InstanceCache, + Request, + RequestStateService, + SaveRecordMutation, +} 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'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; +import type { CollectionResourceDocument, SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; + +import { upgradeStore } from '../-private'; import { assertIdentifierHasId } from './identifier-has-id'; import { payloadIsNotBlank } from './legacy-data-utils'; +import type { AdapterPayload, MinimumAdapterInterface } from './minimum-adapter-interface'; +import type { MinimumSerializerInterface } from './minimum-serializer-interface'; import { normalizeResponseHelper } from './serializer-response'; -import Snapshot from './snapshot'; +import { Snapshot } from './snapshot'; +import { _objectIsAlive } from './utils'; +type Deferred = ReturnType>; type AdapterErrors = Error & { errors?: string[]; isAdapterError?: true }; type SerializerWithParseErrors = MinimumSerializerInterface & { - extractErrors?(store: Store, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): unknown; + extractErrors?(store: Store, modelClass: ModelSchema, error: AdapterErrors, recordId: string | null): unknown; }; -export const SaveOp: unique symbol = Symbol('SaveOp'); +export const SaveOp = getOrSetGlobal('SaveOp', Symbol('SaveOp')); -export type FetchMutationOptions = FindOptions & { [SaveOp]: 'createRecord' | 'deleteRecord' | 'updateRecord' }; +export type FetchMutationOptions = FindRecordOptions & { [SaveOp]: 'createRecord' | 'deleteRecord' | 'updateRecord' }; interface PendingFetchItem { identifier: StableExistingRecordIdentifier; queryRequest: Request; // eslint-disable-next-line @typescript-eslint/no-explicit-any resolver: Deferred; - options: FindOptions; + options: FindRecordOptions; trace?: unknown; promise: Promise; } @@ -52,12 +55,12 @@ interface PendingSaveItem { // eslint-disable-next-line @typescript-eslint/no-explicit-any resolver: Deferred; snapshot: Snapshot; - identifier: RecordIdentifier; + identifier: StableRecordIdentifier; options: FetchMutationOptions; queryRequest: Request; } -export default class FetchManager { +export class FetchManager { declare isDestroyed: boolean; declare requestCache: RequestStateService; // fetches pending in the runloop, waiting to be coalesced @@ -72,7 +75,9 @@ export default class FetchManager { this.isDestroyed = false; } - createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot { + createSnapshot(identifier: StableRecordIdentifier>, options?: FindRecordOptions): Snapshot; + createSnapshot(identifier: StableRecordIdentifier, options?: FindRecordOptions): Snapshot; + createSnapshot(identifier: StableRecordIdentifier, options: FindRecordOptions = {}): Snapshot { return new Snapshot(options, identifier, this._store); } @@ -84,15 +89,18 @@ export default class FetchManager { @internal */ - scheduleSave(identifier: RecordIdentifier, options: FetchMutationOptions): Promise { - let resolver = createDeferred(); - let query: SaveRecordMutation = { + scheduleSave( + identifier: StableRecordIdentifier, + options: FetchMutationOptions + ): Promise { + const resolver = createDeferred(); + const query: SaveRecordMutation = { op: 'saveRecord', recordIdentifier: identifier, options, }; - let queryRequest: Request = { + const queryRequest: Request = { data: [query], }; @@ -113,25 +121,25 @@ export default class FetchManager { scheduleFetch( identifier: StableExistingRecordIdentifier, - options: FindOptions, + options: FindRecordOptions, request: ImmutableRequestInfo ): Promise { - let query: FindRecordQuery = { + const query: FindRecordQuery = { op: 'findRecord', recordIdentifier: identifier, options, }; - let queryRequest: Request = { + const queryRequest: Request = { data: [query], }; - let pendingFetch = this.getPendingFetch(identifier, options); + const pendingFetch = this.getPendingFetch(identifier, options); if (pendingFetch) { return pendingFetch; } - let modelName = identifier.type; + const modelName = identifier.type; const resolver = createDeferred(); const pendingFetchItem: PendingFetchItem = { @@ -141,7 +149,7 @@ export default class FetchManager { queryRequest, } as PendingFetchItem; - let resolverPromise = resolver.promise; + const resolverPromise = resolver.promise; const store = this._store; const isInitialLoad = !store._instanceCache.recordIsLoaded(identifier); // we don't use isLoading directly because we are the request @@ -155,7 +163,7 @@ export default class FetchManager { // additional data received in the payload // may result in the merging of identifiers (and thus records) - let potentiallyNewIm = store._push(payload, options.reload); + const potentiallyNewIm = store._push(payload, options.reload); if (potentiallyNewIm && !Array.isArray(potentiallyNewIm)) { return potentiallyNewIm; } @@ -164,12 +172,10 @@ export default class FetchManager { }, (error) => { assert(`Async Leak Detected: Expected the store to not be destroyed`, !store.isDestroyed); - const cache = DEPRECATE_V1_RECORD_DATA - ? store._instanceCache.peek({ identifier, bucket: 'resourceCache' }) - : store.cache; + const cache = store.cache; if (!cache || cache.isEmpty(identifier) || isInitialLoad) { let isReleasable = true; - if (HAS_GRAPH_PACKAGE) { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { if (!cache) { const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) .graphFor; @@ -196,7 +202,7 @@ export default class FetchManager { }); } - let fetchesByType = this._pendingFetch; + const fetchesByType = this._pendingFetch; let fetchesById = fetchesByType.get(modelName); if (!fetchesById) { @@ -225,12 +231,12 @@ export default class FetchManager { return promise; } - getPendingFetch(identifier: StableExistingRecordIdentifier, options: FindOptions) { - let pendingFetches = this._pendingFetch.get(identifier.type)?.get(identifier); + getPendingFetch(identifier: StableExistingRecordIdentifier, options: FindRecordOptions) { + const pendingFetches = this._pendingFetch.get(identifier.type)?.get(identifier); // We already have a pending fetch for this if (pendingFetches) { - let matchingPendingFetch = pendingFetches.find((fetch) => isSameRequest(options, fetch.options)); + const matchingPendingFetch = pendingFetches.find((fetch) => isSameRequest(options, fetch.options)); if (matchingPendingFetch) { return matchingPendingFetch.promise; } @@ -249,7 +255,7 @@ export default class FetchManager { fetchDataIfNeededForIdentifier( identifier: StableExistingRecordIdentifier, - options: FindOptions = {}, + options: FindRecordOptions = {}, request: ImmutableRequestInfo ): Promise { // pre-loading will change the isEmpty value @@ -282,9 +288,7 @@ export default class FetchManager { } function _isEmpty(instanceCache: InstanceCache, identifier: StableRecordIdentifier): boolean { - const cache = DEPRECATE_V1_RECORD_DATA - ? instanceCache.__instances.resourceCache.get(identifier) - : instanceCache.cache; + const cache = instanceCache.cache; if (!cache) { return true; } @@ -303,7 +307,7 @@ function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): b return ( !isLoaded && // fulfilled === null && - req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query') + req.getPendingRequestsForRecord(identifier).some((r) => r.type === 'query') ); } @@ -343,7 +347,7 @@ function optionsSatisfies(current: object | undefined, existing: object | undefi } // this function helps resolve whether we have a pending request that we should use instead -function isSameRequest(options: FindOptions = {}, existingOptions: FindOptions = {}) { +function isSameRequest(options: FindRecordOptions = {}, existingOptions: FindRecordOptions = {}) { return ( optionsSatisfies(options.adapterOptions, existingOptions.adapterOptions) && includesSatisfies(options.include, existingOptions.include) @@ -356,8 +360,8 @@ function _findMany( modelName: string, snapshots: Snapshot[] ): Promise { - let modelClass = store.modelFor(modelName); // `adapter.findMany` gets the modelClass still - let promise = Promise.resolve().then(() => { + const modelClass = store.modelFor(modelName); // `adapter.findMany` gets the modelClass still + const promise = Promise.resolve().then(() => { const ids = snapshots.map((s) => s.id!); assert( `Cannot fetch a record without an id`, @@ -365,12 +369,11 @@ function _findMany( ); // eslint-disable-next-line @typescript-eslint/unbound-method assert(`Expected this adapter to implement findMany for coalescing`, adapter.findMany); - let ret = adapter.findMany(store, modelClass, ids, snapshots); + const ret = adapter.findMany(store, modelClass, ids, snapshots); assert('adapter.findMany returned undefined, this was very likely a mistake', ret !== undefined); return ret; }); - - promise = guardDestroyedStore(promise, store) as Promise; + upgradeStore(store); return promise.then((adapterPayload) => { assert( @@ -379,16 +382,16 @@ function _findMany( .join(',')}]', but the adapter's response did not have any data`, !!payloadIsNotBlank(adapterPayload) ); - let serializer = store.serializerFor(modelName); - let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findMany'); + const serializer = store.serializerFor(modelName); + const payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findMany'); return payload as CollectionResourceDocument; }); } -function rejectFetchedItems(fetchMap: Map, snapshots: Snapshot[], error?) { +function rejectFetchedItems(fetchMap: Map, snapshots: Snapshot[], error?: Error) { for (let i = 0, l = snapshots.length; i < l; i++) { - let snapshot = snapshots[i]; - let pair = fetchMap.get(snapshot); + const snapshot = snapshots[i]; + const pair = fetchMap.get(snapshot); if (pair) { pair.resolver.reject( @@ -419,9 +422,9 @@ function handleFoundRecords( options object, we resolve all snapshots by id with the first response we see. */ - let snapshotsById = new Map(); + const snapshotsById = new Map(); for (let i = 0; i < snapshots.length; i++) { - let id = snapshots[i].id!; + const id = snapshots[i].id!; let snapshotGroup = snapshotsById.get(id); if (!snapshotGroup) { snapshotGroup = []; @@ -433,10 +436,10 @@ function handleFoundRecords( const included = Array.isArray(coalescedPayload.included) ? coalescedPayload.included : []; // resolve found records - let resources = coalescedPayload.data; + const resources = coalescedPayload.data; for (let i = 0, l = resources.length; i < l; i++) { - let resource = resources[i]; - let snapshotGroup = snapshotsById.get(resource.id); + const resource = resources[i]; + const snapshotGroup = snapshotsById.get(resource.id); snapshotsById.delete(resource.id); if (!snapshotGroup) { @@ -444,8 +447,8 @@ function handleFoundRecords( included.push(resource); } else { snapshotGroup.forEach((snapshot) => { - let pair = fetchMap.get(snapshot)!; - let resolver = pair.resolver; + const pair = fetchMap.get(snapshot)!; + const resolver = pair.resolver; resolver.resolve({ data: resource }); }); } @@ -460,9 +463,9 @@ function handleFoundRecords( } // reject missing records - let rejected: Snapshot[] = []; - snapshotsById.forEach((snapshots) => { - rejected.push(...snapshots); + const rejected: Snapshot[] = []; + snapshotsById.forEach((snapshotArray) => { + rejected.push(...snapshotArray); }); warn( 'Ember Data expected to find records with the following ids in the adapter response from findMany but they were missing: [ "' + @@ -477,8 +480,9 @@ function handleFoundRecords( } function _fetchRecord(store: Store, adapter: MinimumAdapterInterface, fetchItem: PendingFetchItem) { - let identifier = fetchItem.identifier; - let modelName = identifier.type; + upgradeStore(store); + const identifier = fetchItem.identifier; + const modelName = identifier.type; assert(`You tried to find a record but you have no adapter (for ${modelName})`, adapter); assert( @@ -486,22 +490,22 @@ function _fetchRecord(store: Store, adapter: MinimumAdapterInterface, fetchItem: typeof adapter.findRecord === 'function' ); - let snapshot = store._fetchManager.createSnapshot(identifier, fetchItem.options); - let klass = store.modelFor(identifier.type); - let id = identifier.id; + const snapshot = store._fetchManager.createSnapshot(identifier, fetchItem.options); + const klass = store.modelFor(identifier.type); + const id = identifier.id; let promise = Promise.resolve().then(() => { return adapter.findRecord(store, klass, identifier.id, snapshot); }); promise = promise.then((adapterPayload) => { - assert(`Async Leak Detected: Expected the store to not be destroyed`, _objectIsAlive(store)); + assert(`Async Leak Detected: Expected the store to not be destroyed`, !(store.isDestroyed || store.isDestroying)); assert( `You made a 'findRecord' request for a '${modelName}' with id '${id}', but the adapter's response did not have any data`, !!payloadIsNotBlank(adapterPayload) ); - let serializer = store.serializerFor(modelName); - let payload = normalizeResponseHelper(serializer, store, klass, adapterPayload, id, 'findRecord'); + const serializer = store.serializerFor(modelName); + const payload = normalizeResponseHelper(serializer, store, klass, adapterPayload, id, 'findRecord'); assert( `Ember Data expected the primary data returned from a 'findRecord' response to be an object but instead it found an array.`, !Array.isArray(payload.data) @@ -537,7 +541,7 @@ function _processCoalescedGroup( .then((payloads: CollectionResourceDocument) => { handleFoundRecords(store, fetchMap, group, payloads); }) - .catch((error) => { + .catch((error: Error) => { rejectFetchedItems(fetchMap, group, error); }); } else if (group.length === 1) { @@ -552,8 +556,9 @@ function _flushPendingFetchForType( pendingFetchMap: Map, modelName: string ) { - let adapter = store.adapterFor(modelName); - let shouldCoalesce = !!adapter.findMany && adapter.coalesceFindRequests; + upgradeStore(store); + const adapter = store.adapterFor(modelName); + const shouldCoalesce = !!adapter.findMany && adapter.coalesceFindRequests; if (shouldCoalesce) { const pendingFetchItems: PendingFetchItem[] = []; @@ -567,13 +572,13 @@ function _flushPendingFetchForType( pendingFetchItems.push(requestsForIdentifier[0]); }); - let totalItems = pendingFetchItems.length; + const totalItems = pendingFetchItems.length; if (totalItems > 1) { - let snapshots = new Array(totalItems); - let fetchMap = new Map(); + const snapshots = new Array(totalItems); + const fetchMap = new Map(); for (let i = 0; i < totalItems; i++) { - let fetchItem = pendingFetchItems[i]; + const fetchItem = pendingFetchItems[i]; snapshots[i] = store._fetchManager.createSnapshot(fetchItem.identifier, fetchItem.options); fetchMap.set(snapshots[i], fetchItem); } @@ -589,24 +594,25 @@ function _flushPendingFetchForType( _processCoalescedGroup(store, fetchMap, groups[i], adapter, modelName); } } else if (totalItems === 1) { - void _fetchRecord(store, adapter, pendingFetchItems[0]); + _fetchRecord(store, adapter, pendingFetchItems[0]); } } pendingFetchMap.forEach((pendingFetchItems) => { pendingFetchItems.forEach((pendingFetchItem) => { - void _fetchRecord(store, adapter, pendingFetchItem); + _fetchRecord(store, adapter, pendingFetchItem); }); }); } function _flushPendingSave(store: Store, pending: PendingSaveItem) { const { snapshot, resolver, identifier, options } = pending; + upgradeStore(store); const adapter = store.adapterFor(identifier.type); const operation = options[SaveOp]; - let modelName = snapshot.modelName; - let modelClass = store.modelFor(modelName); + 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); @@ -616,7 +622,7 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { ); let promise: Promise = Promise.resolve().then(() => adapter[operation](store, modelClass, snapshot)); - let serializer: SerializerWithParseErrors | null = store.serializerFor(modelName); + const serializer: SerializerWithParseErrors | null = store.serializerFor(modelName); assert( `Your adapter's '${operation}' method must return a value, but it returned 'undefined'`, @@ -624,7 +630,6 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { ); promise = promise.then((adapterPayload) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (!_objectIsAlive(record)) { if (DEPRECATE_RSVP_PROMISE) { deprecate( diff --git a/packages/legacy-compat/src/legacy-network-handler/identifier-has-id.ts b/packages/legacy-compat/src/legacy-network-handler/identifier-has-id.ts index c48b5d3b376..79b6c190038 100644 --- a/packages/legacy-compat/src/legacy-network-handler/identifier-has-id.ts +++ b/packages/legacy-compat/src/legacy-network-handler/identifier-has-id.ts @@ -1,9 +1,9 @@ -import { assert } from '@ember/debug'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -export function assertIdentifierHasId( - identifier: StableRecordIdentifier -): asserts identifier is StableExistingRecordIdentifier { - assert(`Attempted to schedule a fetch for a record without an id.`, identifier.id !== null); +export function assertIdentifierHasId(identifier: unknown): asserts identifier is StableExistingRecordIdentifier { + assert( + `Attempted to schedule a fetch for a record without an id.`, + identifier && (identifier as StableExistingRecordIdentifier).id !== null + ); } diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.js b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.js deleted file mode 100644 index 5f0667f0902..00000000000 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.js +++ /dev/null @@ -1,333 +0,0 @@ -import { assert, deprecate } from '@ember/debug'; - -import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, DEPRECATE_RSVP_PROMISE } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; - -import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common'; -import { iterateData, payloadIsNotBlank } from './legacy-data-utils'; -import { normalizeResponseHelper } from './serializer-response'; - -export function _findHasMany(adapter, store, identifier, link, relationship, options) { - let promise = Promise.resolve().then(() => { - const snapshot = store._fetchManager.createSnapshot(identifier, options); - let useLink = !link || typeof link === 'string'; - let relatedLink = useLink ? link : link.href; - return adapter.findHasMany(store, snapshot, relatedLink, relationship); - }); - - 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.key}' relationship, using link '${link}' , but the adapter's response did not have any data`, - payloadIsNotBlank(adapterPayload) - ); - const modelClass = store.modelFor(relationship.type); - - let serializer = store.serializerFor(relationship.type); - let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany'); - - assert( - `fetched the hasMany relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: [] }`, - 'data' in payload && Array.isArray(payload.data) - ); - - payload = syncRelationshipDataFromLink(store, payload, identifier, relationship); - return store._push(payload, true); - }, - null, - `DS: Extract payload of '${identifier.type}' : hasMany '${relationship.type}'` - ); - - if (DEPRECATE_RSVP_PROMISE) { - const record = store._instanceCache.getRecord(identifier); - - promise = _guard(promise, _bind(_objectIsAlive, record)); - } - - return promise; -} - -export function _findBelongsTo(store, identifier, link, relationship, options) { - let promise = Promise.resolve().then(() => { - let adapter = store.adapterFor(identifier.type); - assert(`You tried to load a belongsTo relationship but you have no adapter (for ${identifier.type})`, adapter); - assert( - `You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'`, - typeof adapter.findBelongsTo === 'function' - ); - let snapshot = store._fetchManager.createSnapshot(identifier, options); - let useLink = !link || typeof link === 'string'; - let relatedLink = useLink ? link : link.href; - 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)); - } - - promise = 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', - }, - } - ); - } - } - - let modelClass = store.modelFor(relationship.type); - let serializer = store.serializerFor(relationship.type); - let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo'); - - assert( - `fetched the belongsTo relationship '${relationship.name}' for ${identifier.type}:${identifier.id} with link '${link}', but no data member is present in the response. If no data exists, the response should set { data: null }`, - 'data' in payload && - (payload.data === null || (typeof payload.data === 'object' && !Array.isArray(payload.data))) - ); - - if (!payload.data && !payload.links && !payload.meta) { - return null; - } - - payload = syncRelationshipDataFromLink(store, payload, identifier, relationship); - - return store._push(payload, true); - }, - null, - `DS: Extract payload of ${identifier.type} : ${relationship.type}` - ); - - return promise; -} - -// sync -// iterate over records in payload.data -// for each record -// assert that record.relationships[inverse] is either undefined (so we can fix it) -// or provide a data: {id, type} that matches the record that requested it -// return the relationship data for the parent -function syncRelationshipDataFromLink(store, payload, parentIdentifier, relationship) { - // ensure the right hand side (incoming payload) points to the parent record that - // requested this relationship - let relationshipData = payload.data - ? iterateData(payload.data, (data, index) => { - const { id, type } = data; - ensureRelationshipIsSetToParent(data, parentIdentifier, store, relationship, index); - return { id, type }; - }) - : null; - - const relatedDataHash = {}; - - if ('meta' in payload) { - relatedDataHash.meta = payload.meta; - } - if ('links' in payload) { - relatedDataHash.links = payload.links; - } - if ('data' in payload) { - relatedDataHash.data = relationshipData; - } - - // now, push the left hand side (the parent record) to ensure things are in sync, since - // the payload will be pushed with store._push - const parentPayload = { - id: parentIdentifier.id, - type: parentIdentifier.type, - relationships: { - [relationship.key]: relatedDataHash, - }, - }; - - if (!Array.isArray(payload.included)) { - payload.included = []; - } - payload.included.push(parentPayload); - - return payload; -} - -function ensureRelationshipIsSetToParent(payload, parentIdentifier, store, parentRelationship, index) { - let { id, type } = payload; - - if (!payload.relationships) { - payload.relationships = {}; - } - let { relationships } = payload; - - let inverse = getInverse(store, parentIdentifier, parentRelationship, type); - if (inverse) { - let { inverseKey, kind } = inverse; - - let relationshipData = relationships[inverseKey] && relationships[inverseKey].data; - - if (DEBUG) { - if ( - typeof relationshipData !== 'undefined' && - !relationshipDataPointsToParent(relationshipData, parentIdentifier) - ) { - let inspect = function inspect(thing) { - return `'${JSON.stringify(thing)}'`; - }; - let quotedType = inspect(type); - let quotedInverse = inspect(inverseKey); - let expected = inspect({ - id: parentIdentifier.id, - type: parentIdentifier.type, - }); - let expectedModel = `${parentIdentifier.type}:${parentIdentifier.id}`; - let got = inspect(relationshipData); - let prefix = typeof index === 'number' ? `data[${index}]` : `data`; - let path = `${prefix}.relationships.${inverseKey}.data`; - let other = relationshipData ? `<${relationshipData.type}:${relationshipData.id}>` : null; - let relationshipFetched = `${expectedModel}.${parentRelationship.kind}("${parentRelationship.name}")`; - let includedRecord = `<${type}:${id}>`; - let message = [ - `Encountered mismatched relationship: Ember Data expected ${path} in the payload from ${relationshipFetched} to include ${expected} but got ${got} instead.\n`, - `The ${includedRecord} record loaded at ${prefix} in the payload specified ${other} as its ${quotedInverse}, but should have specified ${expectedModel} (the record the relationship is being loaded from) as its ${quotedInverse} instead.`, - `This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${parentIdentifier.type} record instead.`, - `Ember Data has corrected the ${includedRecord} record's ${quotedInverse} relationship to ${expectedModel} so that ${relationshipFetched} will include ${includedRecord}.`, - `Please update the response from the server or change your serializer to either ensure that the response for only includes ${quotedType} records that specify ${expectedModel} as their ${quotedInverse}, or omit the ${quotedInverse} relationship from the response.`, - ].join('\n'); - - assert(message); - } - } - - if (kind !== 'hasMany' || typeof relationshipData !== 'undefined') { - relationships[inverseKey] = relationships[inverseKey] || {}; - relationships[inverseKey].data = fixRelationshipData(relationshipData, kind, parentIdentifier); - } - } -} - -function metaIsRelationshipDefinition(meta) { - return typeof meta._inverseKey === 'function'; -} - -function inverseForRelationship(store, identifier, key) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[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 relationship defintion to specify the inverse type or null.`, - definition.options?.inverse === null || - (typeof definition.options?.inverse === 'string' && definition.options.inverse.length > 0) - ); - return definition.options.inverse; -} - -function getInverse(store, parentIdentifier, parentRelationship, type) { - let { name: lhs_relationshipName } = parentRelationship; - let { type: parentType } = parentIdentifier; - let inverseKey = inverseForRelationship(store, { type: parentType }, lhs_relationshipName); - - if (inverseKey) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); - let { kind } = definition[inverseKey]; - return { - inverseKey, - kind, - }; - } -} - -function relationshipDataPointsToParent(relationshipData, identifier) { - if (relationshipData === null) { - return false; - } - - if (Array.isArray(relationshipData)) { - if (relationshipData.length === 0) { - return false; - } - for (let i = 0; i < relationshipData.length; i++) { - let entry = relationshipData[i]; - if (validateRelationshipEntry(entry, identifier)) { - return true; - } - } - } else { - return validateRelationshipEntry(relationshipData, identifier); - } - - return false; -} - -function fixRelationshipData(relationshipData, relationshipKind, { id, type }) { - let parentRelationshipData = { - id, - type, - }; - - let payload; - - if (relationshipKind === 'hasMany') { - payload = relationshipData || []; - if (relationshipData) { - // these arrays could be massive so this is better than filter - // Note: this is potentially problematic if type/id are not in the - // same state of normalization. - let found = relationshipData.find((v) => { - return v.type === parentRelationshipData.type && v.id === parentRelationshipData.id; - }); - if (!found) { - payload.push(parentRelationshipData); - } - } else { - payload.push(parentRelationshipData); - } - } else { - payload = relationshipData || {}; - Object.assign(payload, parentRelationshipData); - } - - return payload; -} - -function validateRelationshipEntry({ id }, { id: parentModelID }) { - return id && id.toString() === parentModelID; -} 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 new file mode 100644 index 00000000000..6864f61fa05 --- /dev/null +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts @@ -0,0 +1,404 @@ +import { deprecate } from '@ember/debug'; + +import type Store from '@ember-data/store'; +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'; + +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, + store: Store, + identifier: StableRecordIdentifier, + link: string | null | { href: string }, + relationship: RelationshipSchema, + options: BaseFinderOptions +): Promise { + upgradeStore(store); + 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; + assert( + `Attempted to load a hasMany relationship from a specified 'link' in the original payload, but the specified link is empty. You must provide a valid 'link' in the original payload to use 'findHasMany'`, + relatedLink + ); + assert( + `Expected the adapter to implement 'findHasMany' but it does not`, + typeof adapter.findHasMany === 'function' + ); + return adapter.findHasMany(store, snapshot, relatedLink, relationship); + }); + + 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 + }' relationship, using link '${JSON.stringify(link)}' , but the adapter's response did not have any data`, + payloadIsNotBlank(adapterPayload) + ); + const modelClass = store.modelFor(relationship.type); + + const serializer = store.serializerFor(relationship.type); + let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany'); + + assert( + `fetched the hasMany relationship '${relationship.name}' for ${identifier.type}:${ + identifier.id + } with link '${JSON.stringify( + link + )}', but no data member is present in the response. If no data exists, the response should set { data: [] }`, + 'data' in payload && Array.isArray(payload.data) + ); + + 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( + store: Store, + identifier: StableRecordIdentifier, + link: string | null | { href: string }, + relationship: RelationshipSchema, + options: BaseFinderOptions +) { + upgradeStore(store); + 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( + `You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'`, + typeof adapter.findBelongsTo === 'function' + ); + const snapshot = store._fetchManager.createSnapshot(identifier, options); + const useLink = !link || typeof link === 'string'; + const relatedLink = useLink ? link : link.href; + assert( + `Attempted to load a belongsTo relationship from a specified 'link' in the original payload, but the specified link is empty. You must provide a valid 'link' in the original payload to use 'findBelongsTo'`, + relatedLink + ); + 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'); + + assert( + `fetched the belongsTo relationship '${relationship.name}' for ${identifier.type}:${ + identifier.id + } with link '${JSON.stringify( + link + )}', but no data member is present in the response. If no data exists, the response should set { data: null }`, + 'data' in payload && (payload.data === null || (typeof payload.data === 'object' && !Array.isArray(payload.data))) + ); + + if (!payload.data && !payload.links && !payload.meta) { + return null; + } + + payload = syncRelationshipDataFromLink(store, payload, identifier as ResourceIdentity, relationship); + + return store._push(payload, true); + }, null); +} + +// sync +// iterate over records in payload.data +// for each record +// assert that record.relationships[inverse] is either undefined (so we can fix it) +// or provide a data: {id, type} that matches the record that requested it +// return the relationship data for the parent +function syncRelationshipDataFromLink( + store: Store, + payload: JsonApiDocument, + parentIdentifier: ResourceIdentity, + relationship: RelationshipSchema +) { + // ensure the right hand side (incoming payload) points to the parent record that + // requested this relationship + const relationshipData = payload.data + ? iterateData(payload.data, (data, index) => { + const { id, type } = data; + ensureRelationshipIsSetToParent(data, parentIdentifier, store, relationship, index); + return { id, type }; + }) + : null; + + const relatedDataHash = {} as JsonApiDocument; + + if ('meta' in payload) { + relatedDataHash.meta = payload.meta; + } + if ('links' in payload) { + relatedDataHash.links = payload.links; + } + if ('data' in payload) { + relatedDataHash.data = relationshipData; + } + + // now, push the left hand side (the parent record) to ensure things are in sync, since + // the payload will be pushed with store._push + const parentPayload = { + id: parentIdentifier.id, + type: parentIdentifier.type, + relationships: { + [relationship.name]: relatedDataHash, + }, + }; + + if (!Array.isArray(payload.included)) { + payload.included = []; + } + payload.included.push(parentPayload); + + return payload; +} + +type ResourceIdentity = { id: string; type: string }; +type RelationshipData = ResourceIdentity | ResourceIdentity[] | null; + +function ensureRelationshipIsSetToParent( + payload: ExistingResourceObject, + parentIdentifier: ResourceIdentity, + store: Store, + parentRelationship: RelationshipSchema, + index: number +) { + const { id, type } = payload; + + if (!payload.relationships) { + payload.relationships = {}; + } + const { relationships } = payload; + + const inverse = getInverse(store, parentIdentifier, parentRelationship, type); + if (inverse) { + const { inverseKey, kind } = inverse; + + const relationshipData = relationships[inverseKey]?.data as RelationshipData | undefined; + + if (DEBUG) { + if ( + typeof relationshipData !== 'undefined' && + !relationshipDataPointsToParent(relationshipData, parentIdentifier) + ) { + const inspect = function inspect(thing: unknown) { + return `'${JSON.stringify(thing)}'`; + }; + const quotedType = inspect(type); + const quotedInverse = inspect(inverseKey); + const expected = inspect({ + id: parentIdentifier.id, + type: parentIdentifier.type, + }); + const expectedModel = `${parentIdentifier.type}:${parentIdentifier.id}`; + const got = inspect(relationshipData); + const prefix = typeof index === 'number' ? `data[${index}]` : `data`; + const path = `${prefix}.relationships.${inverseKey}.data`; + const data = Array.isArray(relationshipData) ? relationshipData[0] : relationshipData; + const other = data ? `<${data.type}:${data.id}>` : null; + const relationshipFetched = `${expectedModel}.${parentRelationship.kind}("${parentRelationship.name}")`; + const includedRecord = `<${type}:${id}>`; + const message = [ + `Encountered mismatched relationship: Ember Data expected ${path} in the payload from ${relationshipFetched} to include ${expected} but got ${got} instead.\n`, + `The ${includedRecord} record loaded at ${prefix} in the payload specified ${other} as its ${quotedInverse}, but should have specified ${expectedModel} (the record the relationship is being loaded from) as its ${quotedInverse} instead.`, + `This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${parentIdentifier.type} record instead.`, + `Ember Data has corrected the ${includedRecord} record's ${quotedInverse} relationship to ${expectedModel} so that ${relationshipFetched} will include ${includedRecord}.`, + `Please update the response from the server or change your serializer to either ensure that the response for only includes ${quotedType} records that specify ${expectedModel} as their ${quotedInverse}, or omit the ${quotedInverse} relationship from the response.`, + ].join('\n'); + + assert(message); + } + } + + if (kind !== 'hasMany' || typeof relationshipData !== 'undefined') { + relationships[inverseKey] = relationships[inverseKey] || {}; + relationships[inverseKey].data = fixRelationshipData(relationshipData ?? null, kind, parentIdentifier); + } + } +} + +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' + ); + assert( + `Expected the relationship defintion to specify the inverse type or null.`, + definition.options?.inverse === null || + (typeof definition.options?.inverse === 'string' && definition.options.inverse.length > 0) + ); + return definition.options.inverse; +} + +function getInverse( + store: Store, + parentIdentifier: ResourceIdentity, + parentRelationship: RelationshipSchema, + type: string +) { + const { name: lhs_relationshipName } = parentRelationship; + const { type: parentType } = parentIdentifier; + const inverseKey = inverseForRelationship(store, { type: parentType }, lhs_relationshipName); + + if (inverseKey) { + const definition = store.schema.fields({ type }).get(inverseKey); + assert( + `Expected the field definition to be a relationship`, + definition && (definition.kind === 'hasMany' || definition.kind === 'belongsTo') + ); + return { + inverseKey, + kind: definition.kind, + }; + } +} + +function relationshipDataPointsToParent(relationshipData: RelationshipData, identifier: ResourceIdentity): boolean { + if (relationshipData === null) { + return false; + } + + if (Array.isArray(relationshipData)) { + if (relationshipData.length === 0) { + return false; + } + for (let i = 0; i < relationshipData.length; i++) { + const entry = relationshipData[i]; + if (validateRelationshipEntry(entry, identifier)) { + return true; + } + } + } else { + return validateRelationshipEntry(relationshipData, identifier); + } + + return false; +} + +function fixRelationshipData( + relationshipData: RelationshipData, + relationshipKind: 'hasMany' | 'belongsTo', + { id, type }: ResourceIdentity +) { + const parentRelationshipData = { + id, + type, + }; + + let payload: { type: string; id: string } | { type: string; id: string }[] | null = null; + + if (relationshipKind === 'hasMany') { + const relData = (relationshipData as { type: string; id: string }[]) || []; + if (relationshipData) { + assert('expected the relationship data to be an array', Array.isArray(relationshipData)); + // these arrays could be massive so this is better than filter + // Note: this is potentially problematic if type/id are not in the + // same state of normalization. + const found = relationshipData.find((v) => { + return v.type === parentRelationshipData.type && v.id === parentRelationshipData.id; + }); + if (!found) { + relData.push(parentRelationshipData); + } + } else { + relData.push(parentRelationshipData); + } + payload = relData; + } else { + const relData = (relationshipData as { type: string; id: string }) || {}; + Object.assign(relData, parentRelationshipData); + payload = relData; + } + + return payload; +} + +function validateRelationshipEntry({ id }: ResourceIdentity, { id: parentModelID }: ResourceIdentity): boolean { + return !!id && id.toString() === parentModelID; +} diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-utils.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-data-utils.ts index c83644a312f..58f4e031bb4 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-utils.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-utils.ts @@ -1,10 +1,12 @@ -import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; +import type { AdapterPayload } from './minimum-adapter-interface'; -export function iterateData(data: T[] | T, fn: (o: T, index?: number) => T) { +type IteratorCB = ((o: T, index: number) => T) | ((o: T) => T); + +export function iterateData(data: T[] | T, fn: IteratorCB) { if (Array.isArray(data)) { return data.map(fn); } else { - return fn(data); + return fn(data, 0); } } diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts index 47abfb8449a..baf9c53e612 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts @@ -1,37 +1,36 @@ -import { assert } from '@ember/debug'; - import { importSync } from '@embroider/macros'; -import { LOG_PAYLOADS } from '@ember-data/debugging'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG, TESTING } from '@ember-data/env'; -import type { Handler, ImmutableRequestInfo, NextFn } from '@ember-data/request/-private/types'; +import type { Future, Handler, NextFn, StructuredDataDocument } from '@ember-data/request'; import type Store from '@ember-data/store'; -import type { StoreRequestContext } from '@ember-data/store/-private/cache-handler'; -import type { Collection } from '@ember-data/store/-private/record-arrays/identifier-array'; -import { SingleResourceDataDocument } from '@ember-data/types/cache/document'; -import type { ModelSchema } from '@ember-data/types/q/ds-model'; +import type { StoreRequestContext } from '@ember-data/store'; +import type { CollectionRecordArray } from '@ember-data/store/-private'; +import type { ModelSchema } from '@ember-data/store/types'; +import { LOG_PAYLOADS } from '@warp-drive/build-config/debugging'; +import { DEBUG, TESTING } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; +import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; import type { CollectionResourceDocument, JsonApiDocument, Links, PaginationLinks, SingleResourceDocument, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { AdapterPayload, MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; -import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; -import type { JsonApiError } from '@ember-data/types/q/record-data-json-api'; -import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; - -import { guardDestroyedStore } from './common'; -import FetchManager, { SaveOp } from './fetch-manager'; +} from '@warp-drive/core-types/spec/json-api-raw'; + +import { upgradeStore } from '../-private'; +import { FetchManager, SaveOp } from './fetch-manager'; import { assertIdentifierHasId } from './identifier-has-id'; import { _findBelongsTo, _findHasMany } from './legacy-data-fetch'; import { payloadIsNotBlank } from './legacy-data-utils'; +import type { MinimumAdapterInterface } from './minimum-adapter-interface'; +import type { MinimumSerializerInterface } from './minimum-serializer-interface'; import { normalizeResponseHelper } from './serializer-response'; -import type Snapshot from './snapshot'; -import SnapshotRecordArray from './snapshot-record-array'; +import type { Snapshot } from './snapshot'; +import { SnapshotRecordArray } from './snapshot-record-array'; type AdapterErrors = Error & { errors?: unknown[]; isAdapterError?: true; code?: string }; type SerializerWithParseErrors = MinimumSerializerInterface & { @@ -51,13 +50,14 @@ const PotentialLegacyOperations = new Set([ ]); export const LegacyNetworkHandler: Handler = { - request(context: StoreRequestContext, next: NextFn): Promise { + request(context: StoreRequestContext, next: NextFn): Future | Promise> { // if we are not a legacy request, move on if (context.request.url || !context.request.op || !PotentialLegacyOperations.has(context.request.op)) { - return next(context.request) as unknown as Promise; + return next(context.request); } const { store } = context.request; + upgradeStore(store); if (!store._fetchManager) { store._fetchManager = new FetchManager(store); } @@ -82,7 +82,7 @@ export const LegacyNetworkHandler: Handler = { case 'deleteRecord': return saveRecord(context); default: - return next(context.request) as unknown as Promise; + return next(context.request); } }, }; @@ -97,22 +97,24 @@ function findBelongsTo(context: StoreRequestContext): Promise { field: RelationshipSchema; }; const identifier = identifiers?.[0]; + upgradeStore(store); // short circuit if we are already loading - let pendingRequest = + const pendingRequest = identifier && store._fetchManager.getPendingFetch(identifier as StableExistingRecordIdentifier, options); if (pendingRequest) { return pendingRequest as Promise; } if (useLink) { - return _findBelongsTo(store, record, links!.related, field, options) as Promise; + assert(`Expected a related link when calling store.findBelongsTo, found ${String(links)}`, links && links.related); + return _findBelongsTo(store, record, links.related, field, options) as Promise; } assert(`Expected an identifier`, Array.isArray(identifiers) && identifiers.length === 1); const manager = store._fetchManager; - assertIdentifierHasId(identifier!); + assertIdentifierHasId(identifier); return options.reload ? (manager.scheduleFetch(identifier, options, context.request) as Promise) @@ -128,6 +130,7 @@ function findHasMany(context: StoreRequestContext): Promise { useLink: boolean; field: RelationshipSchema; }; + upgradeStore(store); // link case if (useLink) { @@ -148,17 +151,18 @@ function findHasMany(context: StoreRequestContext): Promise { `You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'`, typeof adapter.findHasMany === 'function' ); + assert(`Expected a related link when calling store.findHasMany, found ${String(links)}`, links && links.related); - return _findHasMany(adapter, store, record, links!.related, field, options) as Promise; + return _findHasMany(adapter, store, record, links.related, field, options) as Promise; } // identifiers case - - const fetches = new Array>(identifiers!.length); + assert(`Expected an array of identifiers to fetch`, Array.isArray(identifiers)); + const fetches = new Array>(identifiers.length); const manager = store._fetchManager; - for (let i = 0; i < identifiers!.length; i++) { - let identifier = identifiers![i]; + for (let i = 0; i < identifiers.length; i++) { + const identifier = identifiers[i]; // TODO we probably can be lenient here and return from cache for the isNew case assertIdentifierHasId(identifier); fetches[i] = options.reload @@ -173,6 +177,8 @@ function saveRecord(context: StoreRequestContext): Promise { const { store, data, op: operation } = context.request; const { options, record: identifier } = data as { record: StableRecordIdentifier; options: Record }; + upgradeStore(store); + store.cache.willCommit(identifier, context); const saveOptions = Object.assign( @@ -185,36 +191,26 @@ function saveRecord(context: StoreRequestContext): Promise { .then((payload) => { if (LOG_PAYLOADS) { try { - let data: unknown = payload ? JSON.parse(JSON.stringify(payload)) : payload; + const payloadCopy: unknown = payload ? JSON.parse(JSON.stringify(payload)) : payload; // eslint-disable-next-line no-console - console.log(`EmberData | Payload - ${operation!}`, data); - } catch (e) { + console.log(`EmberData | Payload - ${operation}`, payloadCopy); + } catch { // eslint-disable-next-line no-console - console.log(`EmberData | Payload - ${operation!}`, payload); + console.log(`EmberData | Payload - ${operation}`, payload); } } let result: SingleResourceDataDocument; - /* - // TODO @runspired re-evaluate the below claim now that - // the save request pipeline is more streamlined. - - Note to future spelunkers hoping to optimize. - We rely on this `run` to create a run loop if needed - that `store._push` and `store.saveRecord` will both share. - - We use `join` because it is often the case that we - have an outer run loop available still from the first - call to `store._push`; - */ store._join(() => { - //We first make sure the primary data has been updated - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; - result = cache.didCommit(identifier, { request: context.request, content: payload }); - - if (payload && payload.included) { - store._push({ data: null, included: payload.included }, true); - } + // @ts-expect-error we don't have access to a response in legacy + result = store.cache.didCommit(identifier, { request: context.request, content: payload }); }); + + // blatantly lie if we were a createRecord request + // to give some semblance of cache-control to the + // CachePolicy while legacy is still around + if (store.lifetimes?.didRequest && operation === 'createRecord') { + store.lifetimes.didRequest(context.request, { status: 201 } as Response, null, store); + } return store.peekRecord(result!.data!); }) .catch((e: unknown) => { @@ -232,32 +228,35 @@ function saveRecord(context: StoreRequestContext): Promise { function adapterDidInvalidate( store: Store, identifier: StableRecordIdentifier, - error: Error & { errors?: JsonApiError[]; isAdapterError?: true; code?: string } + error: Error & { errors?: ApiError[]; isAdapterError?: true; code?: string } ) { + upgradeStore(store); if (error && error.isAdapterError === true && error.code === 'InvalidError') { - let serializer = store.serializerFor(identifier.type) as SerializerWithParseErrors; + const serializer = store.serializerFor(identifier.type) as SerializerWithParseErrors; // TODO @deprecate extractErrors being called // TODO remove extractErrors from the default serializers. if (serializer && typeof serializer.extractErrors === 'function') { - let errorsHash = serializer.extractErrors(store, store.modelFor(identifier.type), error, identifier.id) as Record< - string, - string | string[] - >; + const errorsHash = serializer.extractErrors( + store, + store.modelFor(identifier.type), + error, + identifier.id + ) as Record; error.errors = errorsHashToArray(errorsHash); } } - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const cache = store.cache; if (error.errors) { assert( `Expected the cache in use by resource ${String( identifier - )} to have a getErrors(identifier) method for retreiving errors.`, + )} to have a getErrors(identifier) method for retrieving errors.`, typeof cache.getErrors === 'function' ); - let jsonApiErrors: JsonApiError[] = error.errors; + let jsonApiErrors: ApiError[] = error.errors; if (jsonApiErrors.length === 0) { jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }]; } @@ -272,12 +271,12 @@ function makeArray(value: T | T[]): T[] { } const PRIMARY_ATTRIBUTE_KEY = 'base'; -function errorsHashToArray(errors: Record): JsonApiError[] { - const out: JsonApiError[] = []; +function errorsHashToArray(errors: Record): ApiError[] { + const out: ApiError[] = []; if (errors) { Object.keys(errors).forEach((key) => { - let messages = makeArray(errors[key]); + const messages = makeArray(errors[key]); for (let i = 0; i < messages.length; i++) { let title = 'Invalid Attribute'; let pointer = `/data/attributes/${key}`; @@ -305,6 +304,7 @@ function findRecord(context: StoreRequestContext): Promise { record: StableExistingRecordIdentifier; options: { reload?: boolean; backgroundReload?: boolean }; }; + upgradeStore(store); let promise: Promise; // if not loaded start loading @@ -318,7 +318,7 @@ function findRecord(context: StoreRequestContext): Promise { promise = store._fetchManager.scheduleFetch(identifier, options, context.request); } else { let snapshot: Snapshot | null = null; - let adapter = store.adapterFor(identifier.type); + const adapter = store.adapterFor(identifier.type); // Refetch the record if the adapter thinks the record is stale if ( @@ -367,7 +367,7 @@ function findRecord(context: StoreRequestContext): Promise { } } - return promise.then((identifier: StableRecordIdentifier) => store.peekRecord(identifier)) as Promise; + return promise.then((i: StableRecordIdentifier) => store.peekRecord(i)) as Promise; } function findAll(context: StoreRequestContext): Promise { @@ -376,6 +376,7 @@ function findAll(context: StoreRequestContext): Promise { type: string; options: { reload?: boolean; backgroundReload?: boolean }; }; + upgradeStore(store); const adapter = store.adapterFor(type); assert(`You tried to load all records but you have no adapter (for ${type})`, adapter); @@ -396,6 +397,7 @@ function findAll(context: StoreRequestContext): Promise { let fetch: Promise | undefined; if (shouldReload) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions maybeRecordArray && (maybeRecordArray.isUpdating = true); fetch = _findAll(adapter, store, type, snapshotArray, context.request, true); } else { @@ -406,6 +408,7 @@ function findAll(context: StoreRequestContext): Promise { (options.backgroundReload !== false && (!adapter.shouldBackgroundReloadAll || adapter.shouldBackgroundReloadAll(store, snapshotArray))) ) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions maybeRecordArray && (maybeRecordArray.isUpdating = true); void _findAll(adapter, store, type, snapshotArray, context.request, false); } @@ -426,13 +429,13 @@ function _findAll( let promise: Promise = Promise.resolve().then(() => adapter.findAll(store, schema, null, snapshotArray) ) as Promise; - promise = guardDestroyedStore(promise, store) as Promise; promise = promise.then((adapterPayload: T) => { assert( `You made a 'findAll' request for '${type}' records, but the adapter's response did not have any data`, payloadIsNotBlank(adapterPayload) ); + upgradeStore(store); const serializer = store.serializerFor(type); const payload = normalizeResponseHelper(serializer, store, schema, adapterPayload, null, 'findAll'); @@ -449,7 +452,7 @@ function _findAll( if (TESTING) { if (!request.disableTestWaiter) { const { waitForPromise } = importSync('@ember/test-waiters') as { - waitForPromise: (promise: Promise) => Promise; + waitForPromise: (promise: Promise) => Promise; }; promise = waitForPromise(promise); } @@ -460,13 +463,15 @@ function _findAll( function query(context: StoreRequestContext): Promise { const { store, data } = context.request; + upgradeStore(store); let { options } = data as { - options: { _recordArray?: Collection; adapterOptions?: Record }; + options: { _recordArray?: CollectionRecordArray; adapterOptions?: Record }; }; + // eslint-disable-next-line @typescript-eslint/no-shadow const { type, query } = data as { type: string; query: Record; - options: { _recordArray?: Collection; adapterOptions?: Record }; + options: { _recordArray?: CollectionRecordArray; adapterOptions?: Record }; }; const adapter = store.adapterFor(type); @@ -487,9 +492,7 @@ function query(context: StoreRequestContext): Promise { delete options._recordArray; } const schema = store.modelFor(type); - let promise = Promise.resolve().then(() => adapter.query(store, schema, query, recordArray, options)); - - promise = guardDestroyedStore(promise, store) as Promise; + const promise = Promise.resolve().then(() => adapter.query(store, schema, query, recordArray, options)); return promise.then((adapterPayload) => { const serializer = store.serializerFor(type); @@ -523,7 +526,9 @@ function assertSingleResourceDocument(payload: JsonApiDocument): asserts payload function queryRecord(context: StoreRequestContext): Promise { const { store, data } = context.request; + // eslint-disable-next-line @typescript-eslint/no-shadow const { type, query, options } = data as { type: string; query: Record; options: object }; + upgradeStore(store); const adapter = store.adapterFor(type); assert(`You tried to make a query but you have no adapter (for ${type})`, adapter); @@ -533,9 +538,7 @@ function queryRecord(context: StoreRequestContext): Promise { ); const schema = store.modelFor(type); - let promise = Promise.resolve().then(() => adapter.queryRecord(store, schema, query, options)) as Promise; - - promise = guardDestroyedStore(promise, store) as Promise; + const promise = Promise.resolve().then(() => adapter.queryRecord(store, schema, query, options)) as Promise; return promise.then((adapterPayload: T) => { const serializer = store.serializerFor(type); diff --git a/ember-data-types/q/minimum-adapter-interface.ts b/packages/legacy-compat/src/legacy-network-handler/minimum-adapter-interface.ts similarity index 97% rename from ember-data-types/q/minimum-adapter-interface.ts rename to packages/legacy-compat/src/legacy-network-handler/minimum-adapter-interface.ts index 47b3392bde6..478d0e36dab 100644 --- a/ember-data-types/q/minimum-adapter-interface.ts +++ b/packages/legacy-compat/src/legacy-network-handler/minimum-adapter-interface.ts @@ -1,13 +1,13 @@ /** * @module @ember-data/experimental-preview-types */ -import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; -import type { Collection } from '@ember-data/store/-private/record-arrays/identifier-array'; +import type { CollectionRecordArray } from '@ember-data/store/-private'; +import type { ModelSchema } from '@ember-data/store/types'; +import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; -import type { ModelSchema } from './ds-model'; -import type { RelationshipSchema } from './record-data-schemas'; -import type { Dict } from './utils'; +import type { Snapshot } from './snapshot'; +import type { SnapshotRecordArray } from './snapshot-record-array'; type Group = Snapshot[]; // TODO this should probably just alias unknown @@ -15,7 +15,7 @@ type Group = Snapshot[]; // however those deserialization cases are handled // far easier in the adapter itself and are unlikely // to be passed to the serializer today. -export type AdapterPayload = Dict | unknown[]; +export type AdapterPayload = Record | unknown[]; /** *
@@ -129,15 +129,15 @@ export interface MinimumAdapterInterface { * @param {ModelSchema} schema An object with methods for accessing information about * the type, attributes and relationships of the primary type associated with the request. * @param {object} query - * @param {Collection} recordArray + * @param {CollectionRecordArray} recordArray * @param {object} options * @return {Promise} a promise resolving with resource data to feed to the associated serializer */ query( store: Store, schema: ModelSchema, - query: Dict, - recordArray: Collection, + query: Record, + recordArray: CollectionRecordArray, options: { adapterOptions?: unknown } ): Promise; @@ -167,7 +167,7 @@ export interface MinimumAdapterInterface { queryRecord( store: Store, schema: ModelSchema, - query: Dict, + query: Record, options: { adapterOptions?: unknown } ): Promise; @@ -514,7 +514,7 @@ export interface MinimumAdapterInterface { * The default behavior if this method is not implemented and the option is not specified is to * not reload, the same as a return of `false`. * - * Note: the Promise returned by `store.findAll` resolves to the same RecordArray instance + * Note: the Promise returned by `store.findAll` resolves to the same LiveArray instance * returned by `store.peekAll` for that type, and will include all records in the store for * the given type, including any previously existing records not returned by the reload request. * diff --git a/ember-data-types/q/minimum-serializer-interface.ts b/packages/legacy-compat/src/legacy-network-handler/minimum-serializer-interface.ts similarity index 92% rename from ember-data-types/q/minimum-serializer-interface.ts rename to packages/legacy-compat/src/legacy-network-handler/minimum-serializer-interface.ts index a43244741ab..c0d0f1db0f5 100644 --- a/ember-data-types/q/minimum-serializer-interface.ts +++ b/packages/legacy-compat/src/legacy-network-handler/minimum-serializer-interface.ts @@ -1,17 +1,15 @@ /** @module @ember-data/experimental-preview-types */ -import type { Object as JSONObject } from 'json-typescript'; - -import type { Snapshot } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { JsonApiDocument, SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; -import type { ModelSchema } from './ds-model'; -import type { JsonApiDocument, SingleResourceDocument } from './ember-data-json-api'; import type { AdapterPayload } from './minimum-adapter-interface'; -import type { Dict } from './utils'; +import type { Snapshot } from './snapshot'; -export type OptionsHash = Dict; +export type SerializerOptions = { includeId?: boolean }; export type RequestType = | 'findRecord' | 'queryRecord' @@ -71,7 +69,7 @@ export interface MinimumSerializerInterface { * @param {'findRecord' | 'queryRecord' | 'findAll' | 'findBelongsTo' | 'findHasMany' | 'findMany' | 'query' | 'createRecord' | 'deleteRecord' | 'updateRecord'} requestType The * type of request the Adapter had been asked to perform. * - * @returns {JsonApiDocument} a document following the structure of a JSON:API Document. + * @return {JsonApiDocument} a document following the structure of a JSON:API Document. */ normalizeResponse( store: Store, @@ -108,7 +106,7 @@ export interface MinimumSerializerInterface { * @param {Snapshot} snapshot A Snapshot for the record to serialize * @param {object} [options] */ - serialize(snapshot: Snapshot, options?: OptionsHash): JSONObject; + serialize(snapshot: Snapshot, options?: SerializerOptions): ObjectValue; /** * This method is intended to normalize data into a [JSON:API Document](https://jsonapi.org/format/#document-structure) @@ -160,11 +158,11 @@ export interface MinimumSerializerInterface { * @param {JSONObject} rawPayload Some raw JSON data to be normalized into a JSON:API Resource. * @param {string} [prop] When called by the EmbeddedRecordsMixin this param will be the * property at which the object provided as rawPayload was found. - * @returns {SingleResourceDocument} A JSON:API Document + * @return {SingleResourceDocument} A JSON:API Document * containing a single JSON:API Resource * as its primary data. */ - normalize?(schema: ModelSchema, rawPayload: JSONObject, prop?: string): SingleResourceDocument; + normalize?(schema: ModelSchema, rawPayload: ObjectValue, prop?: string): SingleResourceDocument; /** * When using `JSONAPIAdapter` or `RESTAdapter` this method is called @@ -204,9 +202,9 @@ export interface MinimumSerializerInterface { * the type, attributes and relationships of the primary type associated with the request. * @param {Snapshot} snapshot A Snapshot for the record to serialize * @param [options] - * @returns {void} + * @return {void} */ - serializeIntoHash?(hash: object, schema: ModelSchema, snapshot: Snapshot, options?: OptionsHash): void; + serializeIntoHash?(hash: object, schema: ModelSchema, snapshot: Snapshot, options?: SerializerOptions): void; /** * This method allows for normalization of data when `store.pushPayload` is called @@ -246,11 +244,11 @@ export interface MinimumSerializerInterface { * @public * @optional * @param {Store} store The store service that initiated the request being normalized - * @param {JSONObject} rawPayload The raw JSON response data returned from an API request. + * @param {object} rawPayload The raw JSON response data returned from an API request. * This JSON should be in the API format expected by the serializer. - * @returns {void} + * @return {void} */ - pushPayload?(store: Store, rawPayload: JSONObject): void; + pushPayload?(store: Store, rawPayload: ObjectValue): void; /** * In some situations the serializer may need to perform cleanup when destroyed, diff --git a/packages/legacy-compat/src/legacy-network-handler/serializer-response.ts b/packages/legacy-compat/src/legacy-network-handler/serializer-response.ts index 7b4d03aa465..921e6fe18ea 100644 --- a/packages/legacy-compat/src/legacy-network-handler/serializer-response.ts +++ b/packages/legacy-compat/src/legacy-network-handler/serializer-response.ts @@ -1,11 +1,11 @@ -import { assert } from '@ember/debug'; - -import { DEBUG } from '@ember-data/env'; import type Store from '@ember-data/store'; -import type { ModelSchema } from '@ember-data/types/q/ds-model'; -import type { JsonApiDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; -import type { MinimumSerializerInterface, RequestType } from '@ember-data/types/q/minimum-serializer-interface'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { JsonApiDocument } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { AdapterPayload } from './minimum-adapter-interface'; +import type { MinimumSerializerInterface, RequestType } from './minimum-serializer-interface'; /** This is a helper method that validates a JSON API top-level document @@ -17,7 +17,7 @@ import type { MinimumSerializerInterface, RequestType } from '@ember-data/types/ */ function validateDocumentStructure(doc?: AdapterPayload | JsonApiDocument): asserts doc is JsonApiDocument { if (DEBUG) { - let errors: string[] = []; + const errors: string[] = []; if (!doc || typeof doc !== 'object') { errors.push('Top level of a JSON API document must be an object'); } else { @@ -75,7 +75,7 @@ export function normalizeResponseHelper( id: string | null, requestType: RequestType ): JsonApiDocument { - let normalizedResponse = serializer + const normalizedResponse = serializer ? serializer.normalizeResponse(store, modelClass, payload, id, requestType) : payload; 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 9bbcb83defd..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 @@ -4,32 +4,32 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/deprecations'; import type Store from '@ember-data/store'; +import type { LiveArray } from '@ember-data/store/-private'; import { SOURCE } from '@ember-data/store/-private'; -import type IdentifierArray from '@ember-data/store/-private/record-arrays/identifier-array'; -import type { DSModelSchema, ModelSchema } from '@ember-data/types/q/ds-model'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; +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'; -import type Snapshot from './snapshot'; /** SnapshotRecordArray is not directly instantiable. Instances are provided to consuming application's - adapters for certain requests. + adapters for certain `findAll` requests. @class SnapshotRecordArray @public */ -export default class SnapshotRecordArray { +export class SnapshotRecordArray { declare _snapshots: Snapshot[] | null; declare _type: ModelSchema | null; declare modelName: string; declare __store: Store; - declare adapterOptions?: Dict; - declare include?: string; + declare adapterOptions?: Record; + declare include?: string | string[]; /** SnapshotRecordArray is not directly instantiable. @@ -43,7 +43,7 @@ export default class SnapshotRecordArray { @param {string} type @param options */ - constructor(store: Store, type: string, options: FindOptions = {}) { + constructor(store: Store, type: string, options: FindAllOptions = {}) { this.__store = store; /** An array of snapshots @@ -116,7 +116,7 @@ export default class SnapshotRecordArray { @private @type {Array} */ - get _recordArray(): IdentifierArray { + get _recordArray(): LiveArray { return this.__store.peekAll(this.modelName); } @@ -175,6 +175,7 @@ export default class SnapshotRecordArray { if (this._snapshots !== null) { return this._snapshots; } + upgradeStore(this.__store); const { _fetchManager } = this.__store; this._snapshots = this._recordArray[SOURCE].map((identifier: StableRecordIdentifier) => @@ -206,8 +207,7 @@ if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { since: { available: '4.5.0', enabled: '4.5.0' }, } ); - // @ts-expect-error - return (this as SnapshotRecordArray)._recordArray.type as DSModelSchema; + 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 058c8ccd275..becb2a7524d 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts @@ -1,23 +1,25 @@ /** @module @ember-data/store */ -import { assert, deprecate } from '@ember/debug'; +import { deprecate } from '@ember/debug'; -import { importSync } from '@embroider/macros'; +import { dependencySatisfies, importSync } from '@embroider/macros'; -import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import { HAS_JSON_API_PACKAGE } from '@ember-data/packages'; +import type { CollectionEdge, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import type { ChangedAttributesHash } from '@ember-data/types/q/cache'; -import { DSModelSchema } from '@ember-data/types/q/ds-model'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { OptionsHash } from '@ember-data/types/q/minimum-serializer-interface'; -import type { AttributeSchema, RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; +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'; +import type { ChangedAttributesHash } from '@warp-drive/core-types/cache'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +import { upgradeStore } from '../-private'; +import type { SerializerOptions } from './minimum-serializer-interface'; type RecordId = string | null; @@ -32,21 +34,31 @@ type RecordId = string | null; @class Snapshot @public */ -export default class Snapshot { - declare __attributes: Dict | null; - declare _belongsToRelationships: Dict; - declare _belongsToIds: Dict; - declare _hasManyRelationships: Dict; - declare _hasManyIds: Dict; +export class Snapshot { + declare __attributes: Record | null; + declare _belongsToRelationships: Record; + declare _belongsToIds: Record; + declare _hasManyRelationships: Record; + declare _hasManyIds: Record; declare _changedAttributes: ChangedAttributesHash; - declare identifier: StableRecordIdentifier; - declare modelName: string; + declare identifier: StableRecordIdentifier : string>; + declare modelName: R extends TypedRecordInstance ? TypeFromInstance : string; declare id: string | null; - declare include?: unknown; - declare adapterOptions?: Dict; + declare include?: string | string[]; + 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 @@ -55,16 +67,20 @@ export default class Snapshot { * @param identifier * @param _store */ - constructor(options: FindOptions, identifier: StableRecordIdentifier, store: Store) { + constructor( + options: FindRecordOptions, + identifier: StableRecordIdentifier : string>, + store: Store + ) { this._store = store; this.__attributes = null; - this._belongsToRelationships = Object.create(null) as Dict; - this._belongsToIds = Object.create(null) as Dict; - this._hasManyRelationships = Object.create(null) as Dict; - this._hasManyIds = Object.create(null) as Dict; + this._belongsToRelationships = Object.create(null) as Record; + this._belongsToIds = Object.create(null) as Record; + this._hasManyRelationships = Object.create(null) as Record; + this._hasManyIds = Object.create(null) as Record; - const hasRecord = !!store._instanceCache.peek({ identifier, bucket: 'record' }); + const hasRecord = !!store._instanceCache.peek(identifier); this.modelName = identifier.type; /** @@ -84,6 +100,7 @@ export default class Snapshot { the values. */ if (hasRecord) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this._attributes; } @@ -130,9 +147,7 @@ export default class Snapshot { */ this.modelName = identifier.type; if (hasRecord) { - const cache = DEPRECATE_V1_RECORD_DATA - ? this._store._instanceCache.getResourceCache(identifier) - : this._store.cache; + const cache = this._store.cache; this._changedAttributes = cache.changedAttrs(identifier); } } @@ -151,48 +166,35 @@ export default class Snapshot { @type {Model} @public */ - get record(): RecordInstance | null { - const record = this._store.peekRecord(this.identifier); + get record(): R | null { + const record = this._store.peekRecord(this.identifier); assert( - `Record ${this.identifier.type} ${String(this.identifier.id)} (${ - this.identifier.lid - }) is not yet loaded and thus cannot be accessed from the Snapshot during serialization`, + `Record ${this.identifier.type} ${this.identifier.id} (${this.identifier.lid}) is not yet loaded and thus cannot be accessed from the Snapshot during serialization`, record !== null ); return record; } - get _attributes(): Dict { + get _attributes(): Record { if (this.__attributes !== null) { return this.__attributes; } - const attributes = (this.__attributes = Object.create(null) as Dict); + const attributes = (this.__attributes = Object.create(null) as Record); const { identifier } = this; - const attrs = Object.keys(this._store.getSchemaDefinitionService().attributesDefinitionFor(identifier)); - const cache = DEPRECATE_V1_RECORD_DATA - ? this._store._instanceCache.getResourceCache(identifier) - : this._store.cache; + const attrs = this._store.schema.fields(identifier); + const cache = this._store.cache; - attrs.forEach((keyName) => { - attributes[keyName] = cache.getAttr(identifier, keyName); + attrs.forEach((field, keyName) => { + if (field.kind === 'attribute') { + attributes[keyName] = cache.getAttr(identifier, keyName); + } }); return attributes; } - /** - The type of the underlying record for this snapshot, as a Model. - - @property type - @public - @deprecated - @type {Model} - */ - get isNew(): boolean { - const cache = DEPRECATE_V1_RECORD_DATA - ? this._store._instanceCache.peek({ identifier: this.identifier, bucket: 'resourceCache' }) - : this._store.cache; + const cache = this._store.cache; return cache?.isNew(this.identifier) || false; } @@ -214,7 +216,7 @@ export default class Snapshot { @return {Object} The attribute value or undefined @public */ - attr(keyName: string): unknown { + attr(keyName: keyof R & string): unknown { if (keyName in this._attributes) { return this._attributes[keyName]; } @@ -235,7 +237,7 @@ export default class Snapshot { @return {Object} All attributes of the current snapshot @public */ - attributes(): Dict { + attributes(): Record { return { ...this._attributes }; } @@ -255,16 +257,16 @@ export default class Snapshot { @public */ changedAttributes(): ChangedAttributesHash { - let changedAttributes = Object.create(null) as ChangedAttributesHash; + const changedAttributes = Object.create(null) as ChangedAttributesHash; if (!this._changedAttributes) { return changedAttributes; } - let changedAttributeKeys = Object.keys(this._changedAttributes); + const changedAttributeKeys = Object.keys(this._changedAttributes); for (let i = 0, length = changedAttributeKeys.length; i < length; i++) { - let key = changedAttributeKeys[i]; - changedAttributes[key] = this._changedAttributes[key].slice() as [unknown, unknown]; + const key = changedAttributeKeys[i]; + changedAttributes[key] = this._changedAttributes[key].slice() as [Value | undefined, Value]; } return changedAttributes; @@ -307,9 +309,9 @@ export default class Snapshot { will be returned if the contents of the relationship is unknown. */ belongsTo(keyName: string, options?: { id?: boolean }): Snapshot | RecordId | undefined { - let returnModeIsId = !!(options && options.id); + const returnModeIsId = !!(options && options.id); let result: Snapshot | RecordId | undefined; - let store = this._store; + const store = this._store; if (returnModeIsId === true && keyName in this._belongsToIds) { return this._belongsToIds[keyName]; @@ -319,51 +321,46 @@ export default class Snapshot { return this._belongsToRelationships[keyName]; } - let relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ - keyName - ]; + const relationshipMeta = store.schema.fields({ type: this.modelName }).get(keyName); assert( `Model '${this.identifier.lid}' has no belongsTo relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'belongsTo' ); - // TODO @runspired it seems this code branch would not work with CUSTOM_MODEL_CLASSes - // this check is not a regression in behavior because relationships don't currently - // function without access to intimate API contracts between RecordData and Model. - // This is a requirement we should fix as soon as the relationship layer does not require - // this intimate API usage. - if (!HAS_JSON_API_PACKAGE) { - assert(`snapshot.belongsTo only supported when using the package @ember-data/json-api`); - } + assert( + `snapshot.belongsTo only supported when using the package @ember-data/graph`, + dependencySatisfies('@ember-data/graph', '*') + ); const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; const { identifier } = this; - const relationship = graphFor(this._store).get(identifier, keyName) as BelongsToRelationship; - assert( - `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${ - identifier.id || '' - }, lid: ${identifier.lid} but no such relationship was found.`, - relationship - ); - assert( - `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${ - identifier.id || '' - }, lid: ${identifier.lid} but that relationship is a hasMany.`, - relationship.definition.kind === 'belongsTo' - ); + if (DEBUG) { + const relationship = graphFor(this._store).get(identifier, keyName) as ResourceEdge; + assert( + `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${ + identifier.id || '' + }, lid: ${identifier.lid} but no such relationship was found.`, + relationship + ); + assert( + `You looked up the ${keyName} belongsTo relationship for { type: ${identifier.type}, id: ${ + identifier.id || '' + }, lid: ${identifier.lid} but that relationship is a hasMany.`, + relationship.definition.kind === 'belongsTo' + ); + } - let value = relationship.getData(); - let data = value && value.data; + const value = graphFor(this._store).getData(identifier, keyName); + const data = value && value.data; + upgradeStore(store); - let inverseIdentifier = data ? store.identifierCache.getOrCreateRecordIdentifier(data) : null; + const inverseIdentifier = data ? store.identifierCache.getOrCreateRecordIdentifier(data) : null; if (value && value.data !== undefined) { - const cache = DEPRECATE_V1_RECORD_DATA - ? inverseIdentifier && store._instanceCache.getResourceCache(inverseIdentifier) - : store.cache; + const cache = store.cache; - if (inverseIdentifier && !cache!.isDeleted(inverseIdentifier)) { + if (inverseIdentifier && !cache.isDeleted(inverseIdentifier)) { if (returnModeIsId) { result = inverseIdentifier.id; } else { @@ -414,10 +411,10 @@ export default class Snapshot { undefined will be returned if the contents of the relationship is unknown. */ hasMany(keyName: string, options?: { ids?: boolean }): RecordId[] | Snapshot[] | undefined { - let returnModeIsIds = !!(options && options.ids); + const returnModeIsIds = !!(options && options.ids); let results: RecordId[] | Snapshot[] | undefined; - let cachedIds: RecordId[] | undefined = this._hasManyIds[keyName]; - let cachedSnapshots: Snapshot[] | undefined = this._hasManyRelationships[keyName]; + const cachedIds: RecordId[] | undefined = this._hasManyIds[keyName]; + const cachedSnapshots: Snapshot[] | undefined = this._hasManyRelationships[keyName]; if (returnModeIsIds === true && keyName in this._hasManyIds) { return cachedIds; @@ -427,10 +424,9 @@ export default class Snapshot { return cachedSnapshots; } - let store = this._store; - let relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ - keyName - ]; + const store = this._store; + upgradeStore(store); + const relationshipMeta = store.schema.fields({ type: this.modelName }).get(keyName); assert( `Model '${this.identifier.lid}' has no hasMany relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'hasMany' @@ -441,33 +437,36 @@ export default class Snapshot { // function without access to intimate API contracts between RecordData and Model. // This is a requirement we should fix as soon as the relationship layer does not require // this intimate API usage. - if (!HAS_JSON_API_PACKAGE) { - assert(`snapshot.hasMany only supported when using the package @ember-data/json-api`); - } + assert( + `snapshot.hasMany only supported when using the package @ember-data/graph`, + dependencySatisfies('@ember-data/graph', '*') + ); const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; const { identifier } = this; - const relationship = graphFor(this._store).get(identifier, keyName) as ManyRelationship; - assert( - `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${ - identifier.id || '' - }, lid: ${identifier.lid} but no such relationship was found.`, - relationship - ); - assert( - `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${ - identifier.id || '' - }, lid: ${identifier.lid} but that relationship is a belongsTo.`, - relationship.definition.kind === 'hasMany' - ); + if (DEBUG) { + const relationship = graphFor(this._store).get(identifier, keyName) as CollectionEdge; + assert( + `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${ + identifier.id || '' + }, lid: ${identifier.lid} but no such relationship was found.`, + relationship + ); + assert( + `You looked up the ${keyName} hasMany relationship for { type: ${identifier.type}, id: ${ + identifier.id || '' + }, lid: ${identifier.lid} but that relationship is a belongsTo.`, + relationship.definition.kind === 'hasMany' + ); + } - let value = relationship.getData(); + const value = graphFor(this._store).getData(identifier, keyName) as CollectionRelationship; if (value.data) { results = []; value.data.forEach((member) => { - let inverseIdentifier = store.identifierCache.getOrCreateRecordIdentifier(member); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(inverseIdentifier) : store.cache; + const inverseIdentifier = store.identifierCache.getOrCreateRecordIdentifier(member); + const cache = store.cache; if (!cache.isDeleted(inverseIdentifier)) { if (returnModeIsIds) { @@ -507,10 +506,12 @@ export default class Snapshot { @param {Object} [binding] the value to which the callback's `this` should be bound @public */ - eachAttribute(callback: (key: string, meta: AttributeSchema) => void, binding?: unknown): void { - let attrDefs = this._store.getSchemaDefinitionService().attributesDefinitionFor(this.identifier); - Object.keys(attrDefs).forEach((key) => { - callback.call(binding, key, attrDefs[key] as AttributeSchema); + eachAttribute(callback: (key: string, meta: LegacyAttributeField) => void, binding?: unknown): void { + const fields = this._store.schema.fields(this.identifier); + fields.forEach((field, key) => { + if (field.kind === 'attribute') { + callback.call(binding, key, field); + } }); } @@ -531,10 +532,12 @@ export default class Snapshot { @param {Object} [binding] the value to which the callback's `this` should be bound @public */ - eachRelationship(callback: (key: string, meta: RelationshipSchema) => void, binding?: unknown): void { - let relationshipDefs = this._store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier); - Object.keys(relationshipDefs).forEach((key) => { - callback.call(binding, key, relationshipDefs[key] as RelationshipSchema); + eachRelationship(callback: (key: string, meta: LegacyRelationshipSchema) => void, binding?: unknown): void { + const fields = this._store.schema.fields(this.identifier); + fields.forEach((field, key) => { + if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + callback.call(binding, key, field); + } }); } @@ -564,7 +567,8 @@ export default class Snapshot { @return {Object} an object whose values are primitive JSON values only @public */ - serialize(options?: OptionsHash): unknown { + serialize(options?: SerializerOptions): unknown { + upgradeStore(this._store); const serializer = this._store.serializerFor(this.modelName); assert(`Cannot serialize record, no serializer found`, serializer); return serializer.serialize(this, options); @@ -584,7 +588,7 @@ if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { since: { available: '4.5.0', enabled: '4.5.0' }, } ); - return this._store.modelFor(this.identifier.type) as DSModelSchema; + 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/legacy-compat/src/utils.ts b/packages/legacy-compat/src/utils.ts new file mode 100644 index 00000000000..d3305706aee --- /dev/null +++ b/packages/legacy-compat/src/utils.ts @@ -0,0 +1,282 @@ +/** + Utilities for helping to migrate to stricter + and more consistent use of IDs and types. + + @module @ember-data/legacy-compat/utils + @main @ember-data/legacy-compat/utils + @deprecated +*/ +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import { assert } from '@warp-drive/build-config/macros'; + +interface AssertFunc { + (desc: string, condition: unknown): asserts condition; + (desc: string): never; +} + +type Reporter = (type: 'formatted-id' | 'formatted-type', actual: unknown, expected: unknown) => void; +type Normalizer = (type: string) => string; + +let MismatchReporter: Reporter = function () {}; + +// TODO: @runspired This pattern prevents AssertFn from being removed in production builds +// but we should enable that if we can. +let _AssertFn: (message: string, condition: unknown) => void = function () {}; +const AssertFn: AssertFunc = ((message: string, condition: unknown) => { + if (!condition) { + _AssertFn(message, condition); + } + assert(message, condition); +}) as unknown as AssertFunc; +let NormalizedType: Normalizer = (str: string) => { + return singularize(dasherize(str)); +}; + +/** + * Configure a function to be called when an id or type + * changes during normalization. This is useful for instrumenting + * to discover places where usage in the app is not consistent. + * + * @method configureMismatchReporter + * @for @ember-data/legacy-compat/utils + * @param method a function which takes a mismatch-type ('formatted-id' | 'formatted-type'), actual, and expected value + * @public + * @static + */ +export function configureMismatchReporter(fn: Reporter): void { + MismatchReporter = fn; +} + +/** + * Configure a function to be called when an id or type + * fails validation. This is useful for instrumenting + * to discover places where usage in the app is not consistent. + * + * @method configureAssertFn + * @for @ember-data/legacy-compat/utils + * @param method a function which takes a message and a condition + * @public + * @static + */ +export function configureAssertFn(fn: (message: string, condition: unknown) => void): void { + _AssertFn = fn; +} + +/** + * Configure a function to be called to normalize + * a resource type string. Used by both formattedType + * and isEquivType to ensure consistent normalization + * during comparison. + * + * If validation fails or the type turns out be unnormalized + * the configured mismatch reporter and assert functions will + * be called. + * + * @method configureTypeNormalization + * @for @ember-data/legacy-compat/utils + * @param method a function which takes a string and returns a string + * @public + * @static + */ +export function configureTypeNormalization(fn: (type: string) => string): void { + NormalizedType = fn; +} + +const NORMALIZED_TYPES = new Map(); + +/** + * Converts a potentially unnormalized type into the format expected + * by our EmberData Cache. Currently this is singular-dasherized. + * + * you should not rely on this function to give you an exact format + * for display purposes. Formatting for display should be handled + * differently if the exact format matters. + * + * Asserts invalid types (undefined, null, '') in dev. + * + * **Usage** + * + * ```js + * import formattedType from 'soxhub-client/helpers/formatted-type'; + * + * formattedType('post'); // => 'post' + * formattedType('posts'); // => 'post' + * formattedType('Posts'); // => 'post' + * formattedType('post-comment'); // => 'post-comment' + * formattedType('post-comments'); // => 'post-comment' + * formattedType('post_comment'); // => 'post-comment' + * formattedType('postComment'); // => 'post-comment' + * formattedType('PostComment'); // => 'post-comment' + * ``` + * + * @method formattedType + * @for @ember-data/legacy-compat/utils + * @param {string} type the potentially un-normalized type + * @return {string} the normalized type + * @public + * @static + */ +export function formattedType(type: T | string): T { + AssertFn('formattedType: type must not be null', type !== null); + AssertFn('formattedType: type must not be undefined', type !== undefined); + AssertFn('formattedType: type must be a string', typeof type === 'string'); + AssertFn('formattedType: type must not be empty', type.length > 0); + let normalized = NORMALIZED_TYPES.get(type); + + if (normalized === undefined) { + normalized = NormalizedType(type); + NORMALIZED_TYPES.set(type, normalized); + } + + if (normalized !== type) { + MismatchReporter('formatted-type', type, normalized); + } + + return normalized as T; +} + +/** + * Format an id to the format expected by the EmberData Cache. + * Currently this means that id should be `string | null`. + * + * Asserts invalid IDs (undefined, '', 0, '0') in dev. + * + * **Usage** + * + * ```js + * import formattedId from 'client/utils/formatted-id'; + * + * formattedId('1'); // => '1' + * formattedId(1); // => '1' + * formattedId(null); // => null + * ``` + * + * @method formattedId + * @for @ember-data/legacy-compat/utils + * @param {string | number | null} id the potentially un-normalized id + * @return {string | null} the normalized id + * @public + * @static + */ +export function formattedId(id: string | number): string; +export function formattedId(id: null): null; +export function formattedId(id: string | number | null): string | null; +export function formattedId(id: string | number | null): string | null { + AssertFn('formattedId: id must not be undefined', id !== undefined); + AssertFn( + 'formattedId: id must be a number, string or null', + typeof id === 'number' || typeof id === 'string' || id === null + ); + AssertFn( + 'formattedId: id must not be empty', + typeof id === 'number' || id === null || (typeof id === 'string' && id.length > 0) + ); + AssertFn('formattedId: id must not be 0', id !== '0' && id !== 0); + + const formatted = id === null ? null : String(id); + if (formatted !== id) { + MismatchReporter('formatted-id', id, formatted); + } + return id === null ? null : String(id); +} + +export function expectId(id: string | number): string; +export function expectId(id: null): never; +export function expectId(id: string | number | null): string { + AssertFn('expectId: id must not be null', id !== null); + + return formattedId(id); +} + +/** + * Compares two types for strict equality, converting them to + * the format expected by the EmberData Cache to ensure + * differences in format are accounted for in the comparison. + * + * Asserts when expected or actual are invalid types in dev. + * Expected may never be null. + * + * ```js + * isEquivType('posts', 'post'); // true + * isEquivType('post', 'post'); // true + * isEquivType('posts', 'posts'); // true + * isEquivType('post-comment', 'postComment'); // true + * isEquivType('post-comment', 'PostComment'); // true + * isEquivType('post-comment', 'post_comment'); // true + * isEquivType('post-comment', 'post-comment'); // true + * isEquivType('post-comment', 'post'); // false + * isEquivType('posts', null); // false + * ``` + * + * @method isEquivType + * @for @ember-data/legacy-compat/utils + * @param {string} expected a potentially unnormalized type to match against + * @param {string} actual a potentially unnormalized type to match against + * @return {boolean} true if the types are equivalent + * @public + * @static + */ +export function isEquivType(expected: string, actual: string): boolean { + AssertFn('isEquivType: Expected type must not be null', expected !== null); + AssertFn('isEquivType: Expected type must not be undefined', expected !== undefined); + AssertFn('isEquivType: Expected type must be a string', typeof expected === 'string'); + AssertFn('isEquivType: Expected type must not be empty', expected.length > 0); + + AssertFn('isEquivType: Actual type must not be null', actual !== null); + AssertFn('isEquivType: Actual type must not be undefined', actual !== undefined); + AssertFn('isEquivType: Actual type must be a string', typeof actual === 'string'); + AssertFn('isEquivType: Actual type must not be empty', actual.length > 0); + + return expected === actual || formattedType(expected) === formattedType(actual); +} + +/** + * Compares two IDs for strict equality, converting them to + * the format expected by the EmberData Cache to ensure + * differences in format are accounted for in the comparison. + * + * Asserts when expected or actual are invalid IDs in dev. + * Expected may never be null. + * + * ```js + * isEquivId('1', 1); // true + * isEquivId('2', '2'); // true + * isEquivId(3, '3'); // true + * isEquivId(4, '3'); // false + * isEquivId(1, null); // false + * ``` + * + * @method isEquivId + * @for @ember-data/legacy-compat/utils + * @param {string | number} expected a potentially un-normalized id to match against + * @param {string | number} actual a potentially un-normalized id to match against + * @return {boolean} true if the ids are equivalent + * @public + * @static + */ +export function isEquivId(expected: string | number, actual: string | number | null): boolean { + AssertFn('isEquivId: Expected id must not be null', expected !== null); + AssertFn('isEquivId: Expected id must not be undefined', expected !== undefined); + AssertFn( + 'isEquivId: Expected id must be a number or string', + typeof expected === 'number' || typeof expected === 'string' + ); + AssertFn( + 'isEquivId: Expected id must not be empty', + typeof expected === 'number' || (typeof expected === 'string' && expected.length > 0) + ); + AssertFn('isEquivId: Expected id must not be 0', expected !== '0' && expected !== 0); + + AssertFn('isEquivId: Actual id must not be undefined', actual !== undefined); + AssertFn( + 'isEquivId: Actual id must be a number, string or null', + typeof actual === 'number' || typeof actual === 'string' || actual === null + ); + AssertFn( + 'isEquivId: Actual id must not be empty', + actual === null || typeof actual === 'number' || (typeof actual === 'string' && actual.length > 0) + ); + AssertFn('isEquivId: Actual id must not be 0', actual !== '0' && actual !== 0); + + return expected === actual || formattedId(expected) === formattedId(actual); +} diff --git a/packages/legacy-compat/tsconfig.json b/packages/legacy-compat/tsconfig.json new file mode 100644 index 00000000000..d3c967cca6f --- /dev/null +++ b/packages/legacy-compat/tsconfig.json @@ -0,0 +1,76 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "noImplicitOverride": false, + "emitDeclarationOnly": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/graph": ["../graph/unstable-preview-types"], + "@ember-data/graph/*": ["../graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../json-api/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../graph" + }, + { + "path": "../json-api" + }, + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/legacy-compat/vite.config.mjs b/packages/legacy-compat/vite.config.mjs new file mode 100644 index 00000000000..ed147cf333a --- /dev/null +++ b/packages/legacy-compat/vite.config.mjs @@ -0,0 +1,12 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = ['@ember/debug', '@ember/application']; +export const entryPoints = ['src/index.ts', 'src/builders.ts', 'src/-private.ts', 'src/utils.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/model/.npmignore b/packages/model/.npmignore deleted file mode 100644 index e4bce62a5ec..00000000000 --- a/packages/model/.npmignore +++ /dev/null @@ -1,40 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/node-tests - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/model/CHANGELOG.md b/packages/model/CHANGELOG.md new file mode 100644 index 00000000000..a9af8b15df5 --- /dev/null +++ b/packages/model/CHANGELOG.md @@ -0,0 +1,98 @@ +# @ember-data/model Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@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)) +* [#9387](https://github.com/emberjs/data/pull/9387) feat: better types for legacy store methods ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#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)) +* [#9245](https://github.com/emberjs/data/pull/9245) feat: add consumer types for Model APIs ([@runspired](https://github.com/runspired)) +* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#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)) +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) +* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@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)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## 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 + +* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@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)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@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)) +* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@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)) + +#### :bug: Bug Fix + +* [#9189](https://github.com/emberjs/data/pull/9189) fix: mutating ManyArray should handle duplicates gracefully (with deprecation) ([@gitKrystan](https://github.com/gitKrystan)) +* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#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)) +* [#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)) +* [#9006](https://github.com/emberjs/data/pull/9006) chore (internal): convert builder and request tests to use diagnostic+runner ([@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)) + +#### Committers: (2) + +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) +Chris Thoburn ([@runspired](https://github.com/runspired)) + diff --git a/packages/model/README.md b/packages/model/README.md index 73ff6764c16..1216e269fbd 100644 --- a/packages/model/README.md +++ b/packages/model/README.md @@ -15,6 +15,25 @@ />

+

Runtime Classes for use as a Schema Source and Resource Presentation for EmberData

+ +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/model +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/model/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/model/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/model/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/model/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/model/lts-4-12?label=%40lts-4-12&color=bbbbbb) + +

Provides a Presentation Model for resource data in an EmberData Cache

This package implements the EmberData Store's `instantiateRecord` and `teardownRecord` hooks diff --git a/packages/model/addon-main.cjs b/packages/model/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/model/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/model/addon-main.js b/packages/model/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/model/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/model/babel.config.js b/packages/model/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/model/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/model/babel.config.mjs b/packages/model/babel.config.mjs new file mode 100644 index 00000000000..c23b859273f --- /dev/null +++ b/packages/model/babel.config.mjs @@ -0,0 +1,12 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ['module:decorator-transforms', { runtime: { import: 'decorator-transforms/runtime' } }], + ], +}; diff --git a/packages/model/blueprints/model-test/index.js b/packages/model/blueprints/model-test/index.js index b01f451f624..8d9e28311b1 100644 --- a/packages/model/blueprints/model-test/index.js +++ b/packages/model/blueprints/model-test/index.js @@ -1,17 +1,17 @@ const path = require('path'); const testInfo = require('ember-cli-test-info'); -const useTestFrameworkDetector = require('@ember-data/private-build-infra/src/utilities/test-framework-detector'); -const modulePrefixForProject = require('@ember-data/private-build-infra/src/utilities/module-prefix-for-project'); +const { dasherize } = require('ember-cli-string-utils'); const ModelBlueprint = require('../model'); -module.exports = useTestFrameworkDetector({ - description: 'Generates a model unit test.', +module.exports = { + description: 'Generates an EmberData Model unit test', + supportsAddon() { return false; }, root: __dirname, - fileMapTokens(options) { + fileMapTokens() { return { __root__() { return 'tests'; @@ -24,10 +24,16 @@ module.exports = useTestFrameworkDetector({ locals(options) { const result = ModelBlueprint.locals.apply(this, arguments); + const modulePrefix = dasherize(options.project.config().modulePrefix); + return { + ...result, + friendlyTestDescription: testInfo.description(options.entity.name, 'Unit', 'Model'), + modulePrefix, + }; + }, - result.friendlyTestDescription = testInfo.description(options.entity.name, 'Unit', 'Model'); - result.modulePrefix = modulePrefixForProject(options.project); + filesPath() { + return path.join(__dirname, 'qunit-files') + } +}; - return result; - }, -}); diff --git a/packages/model/blueprints/model-test/mocha-files/__root__/__path__/__test__.js b/packages/model/blueprints/model-test/mocha-files/__root__/__path__/__test__.js deleted file mode 100644 index e9fd293faa3..00000000000 --- a/packages/model/blueprints/model-test/mocha-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupModelTest } from 'ember-mocha'; - -describe('<%= friendlyTestDescription %>', function () { - setupModelTest('<%= dasherizedModuleName %>', { - // Specify the other units that are required for this test. - <%= typeof needs !== 'undefined' ? needs : '' %>, - }); - - // Replace this with your real tests. - it('exists', function () { - let model = this.subject(); - // var store = this.store(); - expect(model).to.be.ok; - }); -}); diff --git a/packages/model/blueprints/model-test/mocha-rfc-232-files/__root__/__path__/__test__.js b/packages/model/blueprints/model-test/mocha-rfc-232-files/__root__/__path__/__test__.js deleted file mode 100644 index eb948d4fcd7..00000000000 --- a/packages/model/blueprints/model-test/mocha-rfc-232-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from '<%= modulePrefix %>/tests/helpers'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('<%= dasherizedModuleName %>', {}); - expect(model).to.be.ok; - }); -}); diff --git a/packages/model/blueprints/model-test/qunit-files/__root__/__path__/__test__.js b/packages/model/blueprints/model-test/qunit-files/__root__/__path__/__test__.js index 96563d14adc..7d59a55a990 100644 --- a/packages/model/blueprints/model-test/qunit-files/__root__/__path__/__test__.js +++ b/packages/model/blueprints/model-test/qunit-files/__root__/__path__/__test__.js @@ -1,14 +1,13 @@ -import { module, test } from 'qunit'; - import { setupTest } from '<%= modulePrefix %>/tests/helpers'; +import { module, test } from 'qunit'; module('<%= friendlyTestDescription %>', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('<%= dasherizedModuleName %>', {}); - assert.ok(model); + const store = this.owner.lookup('service:store'); + const model = store.createRecord('<%= dasherizedModuleName %>', {}); + assert.ok(model, 'model exists'); }); }); diff --git a/packages/model/blueprints/model/index.js b/packages/model/blueprints/model/index.js index 6fe5aba15f2..aa9330f50ad 100644 --- a/packages/model/blueprints/model/index.js +++ b/packages/model/blueprints/model/index.js @@ -1,17 +1,28 @@ +const path = require('path'); const EOL = require('os').EOL; +const { has } = require('@ember/edition-utils'); + const inflection = require('inflection'); const stringUtils = require('ember-cli-string-utils'); -const useEditionDetector = require('@ember-data/private-build-infra/src/utilities/edition-detector'); -const { has } = require('@ember/edition-utils'); -module.exports = useEditionDetector({ - description: 'Generates an ember-data model.', +module.exports = { + description: 'Generates an ember-data Model.', anonymousOptions: ['name', 'attr:type'], root: __dirname, + filesPath() { + let hasOctane = has('octane'); + if (hasOctane && process.env.EMBER_EDITION === 'classic') { + hasOctane = false; //forcible override + } + let rootPath = hasOctane ? 'native-files' : 'files'; + return path.join(__dirname, rootPath); + }, + + locals(options) { let attrs = []; let needs = []; @@ -109,7 +120,7 @@ module.exports = useEditionDetector({ needs, }; }, -}); +} function nativeAttr(attr) { let name = attr.name, @@ -148,3 +159,5 @@ function classicAttr(attr) { } return propertyName + ': ' + result; } + + diff --git a/packages/model/ember-data-logo-dark.svg b/packages/model/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/model/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/model/ember-data-logo-light.svg b/packages/model/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/model/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/model/eslint.config.mjs b/packages/model/eslint.config.mjs new file mode 100644 index 00000000000..c42683457ca --- /dev/null +++ b/packages/model/eslint.config.mjs @@ -0,0 +1,23 @@ +// @ts-check +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 [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: externals, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/model/package.json b/packages/model/package.json index eeab7549b75..273d3d3159d 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -14,34 +14,48 @@ "author": "", "directories": {}, "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 }, "files": [ + "unstable-preview-types", "blueprints", - "addon-main.js", - "addon", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./blueprints/*": { + "default": "./blueprints/*.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "peerDependencies": { - "@ember-data/debug": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "ember-inflector": "^4.0.2" + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@warp-drive/core-types": "workspace:*" }, "peerDependenciesMeta": { "@ember-data/json-api": { @@ -49,53 +63,73 @@ }, "@ember-data/graph": { "optional": true - }, - "@ember-data/debug": { - "optional": true } }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "ember-cached-decorator-polyfill": "^1.0.1", - "ember-cli-babel": "^7.26.11", + "@embroider/macros": "^1.16.6", "ember-cli-string-utils": "^1.1.0", "ember-cli-test-info": "^1.0.0", - "inflection": "~2.0.1" + "inflection": "~3.0.0", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "typescript": "^5.0.3", - "walk-sync": "^3.0.0", - "webpack": "^5.77.0" + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "decorator-transforms": "^2.2.2", + "ember-source": "~5.12.0", + "expect-type": "^0.20.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/model/rollup.config.mjs b/packages/model/rollup.config.mjs deleted file mode 100644 index 077683142dc..00000000000 --- a/packages/model/rollup.config.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@ember/service', - 'ember', - 'ember-inflector', - '@ember/debug', - '@ember/object/computed', - '@ember/object/compat', - '@ember-data/store/-private', - '@ember-data/store', - '@ember/object/internals', - '@ember-data/tracking/-private', - '@ember/object/promise-proxy-mixin', - '@ember/object/proxy', - '@ember/array', - '@ember/array/proxy', - '@ember/string', - '@ember/object', - '@ember/object/mixin', - '@ember/application', - '@glimmer/env', - '@glimmer/tracking', - '@ember/runloop', - '@ember/polyfills', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'error.js', 'json-api.js', 'rest.js', '-private.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/model/src/-private.ts b/packages/model/src/-private.ts index f2169364752..92b7d2d2c7e 100644 --- a/packages/model/src/-private.ts +++ b/packages/model/src/-private.ts @@ -1,14 +1,13 @@ -export { default as attr } from './-private/attr'; -export { default as belongsTo } from './-private/belongs-to'; -export { default as hasMany } from './-private/has-many'; -export { default as Model } from './-private/model'; -export { default as Errors } from './-private/errors'; +export { attr } from './-private/attr'; +export { belongsTo } from './-private/belongs-to'; +export { hasMany } from './-private/has-many'; +export { Model } from './-private/model'; +export type { ModelStore } from './-private/model'; +export { Errors } from './-private/errors'; -export { default as ManyArray } from './-private/many-array'; -export { default as PromiseBelongsTo } from './-private/promise-belongs-to'; -export { default as PromiseManyArray } from './-private/promise-many-array'; -export { default as _modelForMixin } from './-private/model-for-mixin'; +export { RelatedCollection as ManyArray } from './-private/many-array'; +export { PromiseBelongsTo } from './-private/promise-belongs-to'; +export { PromiseManyArray } from './-private/promise-many-array'; -// // Used by tests -export { default as diffArray } from './-private/diff-array'; -export { LEGACY_SUPPORT } from './-private/model'; +// // Used by tests, migration support +export { lookupLegacySupport, LEGACY_SUPPORT } from './-private/legacy-relationships-support'; diff --git a/packages/model/src/-private/attr.js b/packages/model/src/-private/attr.js deleted file mode 100644 index 151dc9d396a..00000000000 --- a/packages/model/src/-private/attr.js +++ /dev/null @@ -1,165 +0,0 @@ -import { assert } from '@ember/debug'; -import { computed } from '@ember/object'; - -import { DEBUG } from '@ember-data/env'; -import { recordIdentifierFor } from '@ember-data/store'; -import { peekCache } from '@ember-data/store/-private'; - -import { computedMacroWithOptionalParams } from './util'; - -/** - @module @ember-data/model -*/ - -/** - `attr` defines an attribute on a [Model](/ember-data/release/classes/Model). - By default, attributes are passed through as-is, however you can specify an - optional type to have the value automatically transformed. - Ember Data ships with four basic transform types: `string`, `number`, - `boolean` and `date`. You can define your own transforms by subclassing - [Transform](/ember-data/release/classes/Transform). - - Note that you cannot use `attr` to define an attribute of `id`. - - `attr` takes an optional hash as a second parameter, currently - supported options are: - - - `defaultValue`: Pass a string or a function to be called to set the attribute - to a default value if and only if the key is absent from the payload response. - - Example - - ```app/models/user.js - import Model, { attr } from '@ember-data/model'; - - export default class UserModel extends Model { - @attr('string') username; - @attr('string') email; - @attr('boolean', { defaultValue: false }) verified; - } - ``` - - Default value can also be a function. This is useful it you want to return - a new object for each attribute. - - ```app/models/user.js - import Model, { attr } from '@ember-data/model'; - - export default class UserModel extends Model { - @attr('string') username; - @attr('string') email; - - @attr({ - defaultValue() { - return {}; - } - }) - settings; - } - ``` - - The `options` hash is passed as second argument to a transforms' - `serialize` and `deserialize` method. This allows to configure a - transformation and adapt the corresponding value, based on the config: - - ```app/models/post.js - import Model, { attr } from '@ember-data/model'; - - export default class PostModel extends Model { - @attr('text', { - uppercase: true - }) - text; - } - ``` - - ```app/transforms/text.js - export default class TextTransform { - serialize(value, options) { - if (options.uppercase) { - return value.toUpperCase(); - } - - return value; - } - - deserialize(value) { - return value; - } - - static create() { - return new this(); - } - } - ``` - - @method attr - @public - @static - @for @ember-data/model - @param {String|Object} type the attribute type - @param {Object} options a hash of options - @return {Attribute} -*/ -function attr(type, options) { - if (typeof type === 'object') { - options = type; - type = undefined; - } else { - options = options || {}; - } - - let meta = { - type: type, - isAttribute: true, - options: options, - }; - - return computed({ - get(key) { - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` - ); - } - } - if (this.isDestroyed || this.isDestroying) { - return; - } - return peekCache(this).getAttr(recordIdentifierFor(this), key); - }, - set(key, value) { - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` - ); - } - } - assert( - `Attempted to set '${key}' on the deleted record ${recordIdentifierFor(this)}`, - !this.currentState.isDeleted - ); - const identifier = recordIdentifierFor(this); - const cache = peekCache(this); - - let currentValue = cache.getAttr(identifier, key); - if (currentValue !== value) { - cache.setAttr(identifier, key, value); - - if (!this.isValid) { - const { errors } = this; - if (errors.get(key)) { - errors.remove(key); - this.currentState.cleanErrorRequests(); - } - } - } - - return value; - }, - }).meta(meta); -} - -export default computedMacroWithOptionalParams(attr); diff --git a/packages/model/src/-private/attr.ts b/packages/model/src/-private/attr.ts new file mode 100644 index 00000000000..fcc0b2be49b --- /dev/null +++ b/packages/model/src/-private/attr.ts @@ -0,0 +1,295 @@ +/** + @module @ember-data/model +*/ +import { computed } from '@ember/object'; + +import { recordIdentifierFor } from '@ember-data/store'; +import { peekCache } from '@ember-data/store/-private'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { ArrayValue, ObjectValue, PrimitiveValue, Value } from '@warp-drive/core-types/json/raw'; +import type { TransformName } from '@warp-drive/core-types/symbols'; + +import type { Model } from './model'; +import type { DecoratorPropertyDescriptor } from './util'; +import { isElementDescriptor } from './util'; + +/** + * Options provided to the attr decorator are + * supplied to the associated transform. Any + * key-value pair is valid; however, it is highly + * recommended to only use statically defined values + * that could be serialized to JSON. + * + * If no transform is provided, the only valid + * option is `defaultValue`. + * + * Examples: + * + * ```ts + * class User extends Model { + * @attr('string', { defaultValue: 'Anonymous' }) name; + * @attr('date', { defaultValue: () => new Date() }) createdAt; + * @attr({ defaultValue: () => ({}) }) preferences; + * @attr('boolean') hasVerifiedEmail; + * @attr address; + * } + * + * @class NOTATHING + * @typedoc + */ +export type AttrOptions = { + /** + * The default value for this attribute. + * + * Default values can be provided as a value or a function that will be + * executed to generate the default value. + * + * Default values *should not* be stateful (object, arrays, etc.) as + * they will be shared across all instances of the record. + * + * @typedoc + */ + defaultValue?: DV extends PrimitiveValue ? DV : () => DV; +}; + +function _attr(type?: string | AttrOptions, options?: AttrOptions) { + if (typeof type === 'object') { + options = type; + type = undefined; + } else { + options = options || {}; + } + + const meta = { + type: type, + kind: 'attribute', + isAttribute: true, + options: options, + key: null, + }; + + return computed({ + get(this: Model, key: string) { + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` + ); + } + } + if (this.isDestroyed || this.isDestroying) { + return; + } + return peekCache(this).getAttr(recordIdentifierFor(this), key); + }, + set(this: Model, key: string, value: Value) { + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your attr on ${this.constructor.toString()}` + ); + } + } + const identifier = recordIdentifierFor(this); + assert( + `Attempted to set '${key}' on the deleted record ${identifier.type}:${identifier.id} (${identifier.lid})`, + !this.currentState.isDeleted + ); + const cache = peekCache(this); + + const currentValue = cache.getAttr(identifier, key); + if (currentValue !== value) { + cache.setAttr(identifier, key, value); + + if (!this.isValid) { + const { errors } = this; + + if (errors.get(key)) { + errors.remove(key); + this.currentState.cleanErrorRequests(); + } + } + } + + return value; + }, + }).meta(meta); +} + +// NOTE: Usage of Explicit ANY +// ------------------------------------------------------------------- +// any is required here because we are the maximal not the minimal +// subset of options allowed. If we used unknown, object, or +// Record we would get type errors when we try to +// assert against a more specific implementation with precise options. +// ------------------------------------------------------------------- + +type LooseTransformInstance = { + /** + * value type must match the return type of the deserialize method + * + * @typedoc + */ + // see note on Explicit ANY above + // eslint-disable-next-line @typescript-eslint/no-explicit-any + serialize: (value: V, options: any) => Raw; + /** + * defaultValue type must match the return type of the deserialize method + * + * @typedoc + */ + // see note on Explicit ANY above + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deserialize: (value: Raw, options: any) => V; + + [TransformName]: Name; +}; +export type TransformHasType = { [TransformName]: string }; + +export type TypedTransformInstance = + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance + | LooseTransformInstance; + +// see note on Explicit ANY above +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GetMaybeDeserializeValue = T extends { deserialize: (...args: any[]) => unknown } + ? ReturnType + : never; + +export type TypeFromInstance = T extends TransformHasType ? T[typeof TransformName] : never; +export type ExtractOptions, TypeFromInstance>> = + Parameters[1] & Parameters[1] & AttrOptions>; +export type OptionsFromInstance = + TypeFromInstance extends never + ? never + : GetMaybeDeserializeValue extends never + ? never + : T extends TypedTransformInstance, TypeFromInstance> + ? Parameters[1] & Parameters[1] & AttrOptions> + : never; + +/** + * The return type of `void` is a lie to appease TypeScript. The actual return type + * is a descriptor, but typescript incorrectly insists that decorator functions return + * `void` or `any`. + * + * @typedoc + */ +export type DataDecorator = (target: object, key: string, desc?: DecoratorPropertyDescriptor) => void; + +/** + `attr` defines an attribute on a [Model](/ember-data/release/classes/Model). + By default, attributes are passed through as-is, however you can specify an + optional type to have the value automatically transformed. + EmberData ships with four basic transform types: `string`, `number`, + `boolean` and `date`. You can define your own transforms by subclassing + [Transform](/ember-data/release/classes/Transform). + + Note that you cannot use `attr` to define an attribute of `id`. + + `attr` takes an optional hash as a second parameter, currently + supported options are: + + - `defaultValue`: Pass a string or a function to be called to set the attribute + to a default value if and only if the key is absent from the payload response. + + Example + + ```app/models/user.js + import Model, { attr } from '@ember-data/model'; + + export default class UserModel extends Model { + @attr('string') username; + @attr('string') email; + @attr('boolean', { defaultValue: false }) verified; + } + ``` + + Default value can also be a function. This is useful it you want to return + a new object for each attribute. + + ```app/models/user.js + import Model, { attr } from '@ember-data/model'; + + export default class UserModel extends Model { + @attr('string') username; + @attr('string') email; + + @attr({ + defaultValue() { + return {}; + } + }) + settings; + } + ``` + + The `options` hash is passed as second argument to a transforms' + `serialize` and `deserialize` method. This allows to configure a + transformation and adapt the corresponding value, based on the config: + + ```app/models/post.js + import Model, { attr } from '@ember-data/model'; + + export default class PostModel extends Model { + @attr('text', { + uppercase: true + }) + text; + } + ``` + + ```app/transforms/text.js + export default class TextTransform { + serialize(value, options) { + if (options.uppercase) { + return value.toUpperCase(); + } + + return value; + } + + deserialize(value) { + return value; + } + + static create() { + return new this(); + } + } + ``` + + @method attr + @public + @static + @for @ember-data/model + @param {String|Object} type the attribute type + @param {Object} options a hash of options + @return {Attribute} +*/ +export function attr(): DataDecorator; +export function attr(type: TypeFromInstance): DataDecorator; +export function attr(type: string): DataDecorator; +export function attr(options: AttrOptions): DataDecorator; +export function attr(type: TypeFromInstance, options?: OptionsFromInstance): DataDecorator; +export function attr(type: string, options?: AttrOptions & object): DataDecorator; +export function attr(target: object, key: string | symbol, desc?: PropertyDescriptor): void; // see note on DataDecorator for why void +export function attr( + type?: string | AttrOptions | object, + options?: (AttrOptions & object) | string | symbol, + desc?: PropertyDescriptor +): DataDecorator | void { + const args = [type, options, desc]; + // see note on DataDecorator for why void + return isElementDescriptor(args) ? (_attr()(...args) as void) : _attr(type, options as object); +} diff --git a/packages/model/src/-private/attr.type-test.ts b/packages/model/src/-private/attr.type-test.ts new file mode 100644 index 00000000000..40b6d0e5cb7 --- /dev/null +++ b/packages/model/src/-private/attr.type-test.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { expectTypeOf } from 'expect-type'; + +import type { TransformName } from '@warp-drive/core-types/symbols'; + +import type { + AttrOptions, + DataDecorator, + ExtractOptions, + GetMaybeDeserializeValue, + OptionsFromInstance, + TypedTransformInstance, + TypeFromInstance, +} from './attr'; +import { attr } from './attr'; + +// ------------------------------ +// 💚 +// ============================== +// Type Tests +// ============================== +// 🐹 +// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ + +expectTypeOf<{ defaultValue: () => object }>().toMatchTypeOf>(); +expectTypeOf<{ defaultValue: () => object }>().toMatchTypeOf(); +expectTypeOf<{ defaultValue: () => object }>().not.toMatchTypeOf>(); +expectTypeOf<{ defaultValue: () => object }>().not.toMatchTypeOf>(); +expectTypeOf<{ defaultValue: () => string }>().not.toMatchTypeOf>(); + +type ExampleDateTransform = { + serialize(value: Date, options: { dateOnly: boolean }): string; + deserialize(value: string, options: { stripTimeZone?: boolean }): Date; + [TransformName]: 'date'; +}; +type ExampleBooleanTransform = { + deserialize(serialized: boolean | null | number | string, options?: { allowNull?: boolean }): boolean | null; + serialize(deserialized: boolean | null, options?: { allowNull?: boolean }): boolean | null; + [TransformName]: 'boolean'; +}; +type A1 = GetMaybeDeserializeValue; +type A2 = TypeFromInstance; +type A3 = TypedTransformInstance; +type A4 = ExampleBooleanTransform extends A3 ? true : false; +type A5 = ExtractOptions; +type A6 = OptionsFromInstance; + +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toMatchTypeOf<{ allowNull?: boolean } | undefined>(); +expectTypeOf().toMatchTypeOf<{ allowNull?: boolean } | undefined>(); + +expectTypeOf< + TypedTransformInstance, TypeFromInstance> +>().toMatchTypeOf>(); +expectTypeOf().toMatchTypeOf>(); +expectTypeOf>().toEqualTypeOf<'date'>(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toMatchTypeOf<{ + dateOnly: boolean; + stripTimeZone?: boolean; + defaultValue?: () => Date; +}>(); +expectTypeOf({ dateOnly: true }).toMatchTypeOf>(); +expectTypeOf({ dateOnly: true, stripTimeZone: true }).toMatchTypeOf>(); +expectTypeOf({ dateOnly: true, stripTimeZone: true, defaultValue: () => new Date() }).toMatchTypeOf< + OptionsFromInstance +>(); + +expectTypeOf(attr({}, 'key', {})).toEqualTypeOf(); +expectTypeOf(attr('string')).toEqualTypeOf(); +expectTypeOf(attr({})).toEqualTypeOf(); +expectTypeOf(attr()).toEqualTypeOf(); + +expectTypeOf(attr('string', { defaultValue: 'hello' })).toEqualTypeOf(); +expectTypeOf(attr({ defaultValue: 'hello' })).toEqualTypeOf(); +expectTypeOf(attr('string', { defaultValue: () => ({}) })).toEqualTypeOf(); +expectTypeOf(attr({ defaultValue: () => ({}) })).toEqualTypeOf(); + +expectTypeOf>().toEqualTypeOf<'date'>(); +expectTypeOf(attr('date')).toEqualTypeOf(); +/* prettier-ignore */ +expectTypeOf(attr('date', { + dateOnly: true, + // @ts-expect-error - defaultValue needs to be a Date so it can be serialized, so must be a function that produces one + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + defaultValue: string +})).toBeNever; + +/* prettier-ignore */ +expectTypeOf( + // @ts-expect-error + attr( + 'string', + { defaultValue: ({}) } + ) +).toEqualTypeOf(); + +/* prettier-ignore */ +expectTypeOf(attr('date', { + // @ts-expect-error - no stateful default values + defaultValue: new Date() +})).toBeNever; +expectTypeOf( + attr('date', { dateOnly: false, defaultValue: () => new Date() }) +).toEqualTypeOf(); + +/* prettier-ignore */ +expectTypeOf( + attr( + // @ts-expect-error + { defaultValue: {} } + ) +).toBeNever; +expectTypeOf( + attr( + // @ts-expect-error + 1, + { defaultValue: 'hello' } + ) +).toBeNever; + +expectTypeOf( + (function () { + class User { + @attr() declare name: string; + } + return User; + })() +).toMatchTypeOf<{ name: string }>(); +expectTypeOf( + (function () { + class User { + @attr declare name: string; + } + return User; + })() +).toMatchTypeOf<{ name: string }>(); diff --git a/packages/model/src/-private/belongs-to.js b/packages/model/src/-private/belongs-to.js deleted file mode 100644 index 850ebeea42a..00000000000 --- a/packages/model/src/-private/belongs-to.js +++ /dev/null @@ -1,257 +0,0 @@ -import { assert, deprecate, warn } from '@ember/debug'; -import { computed } from '@ember/object'; -import { dasherize } from '@ember/string'; - -import { - DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC, - DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, - DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE, -} from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; - -import { lookupLegacySupport } from './model'; -import { computedMacroWithOptionalParams } from './util'; - -function normalizeType(type) { - if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { - if (!type) { - return; - } - } - - return dasherize(type); -} -/** - @module @ember-data/model -*/ - -/** - `belongsTo` is used to define One-To-One and One-To-Many - relationships on a [Model](/ember-data/release/classes/Model). - - - `belongsTo` takes an optional hash as a second parameter, currently - supported options are: - - - `async`: A boolean value used to explicitly declare this to be an async relationship. The default is true. - - `inverse`: A string used to identify the inverse property on a - related model in a One-To-Many relationship. See [Explicit Inverses](#explicit-inverses) - - `polymorphic` A boolean value to mark the relationship as polymorphic - - #### One-To-One - To declare a one-to-one relationship between two models, use - `belongsTo`: - - ```app/models/user.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class UserModel extends Model { - @belongsTo('profile') profile; - } - ``` - - ```app/models/profile.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class ProfileModel extends Model { - @belongsTo('user') user; - } - ``` - - #### One-To-Many - - To declare a one-to-many relationship between two models, use - `belongsTo` in combination with `hasMany`, like this: - - ```app/models/post.js - import Model, { hasMany } from '@ember-data/model'; - - export default class PostModel extends Model { - @hasMany('comment', { async: false, inverse: 'post' }) comments; - } - ``` - - ```app/models/comment.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class CommentModel extends Model { - @belongsTo('post', { async: false, inverse: 'comments' }) post; - } - ``` - - #### Sync relationships - - Ember Data resolves sync relationships with the related resources - available in its local store, hence it is expected these resources - to be loaded before or along-side the primary resource. - - ```app/models/comment.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class CommentModel extends Model { - @belongsTo('post', { - async: false, - inverse: null - }) - post; - } - ``` - - In contrast to async relationship, accessing a sync relationship - will always return the record (Model instance) for the existing - local resource, or null. But it will error on access when - a related resource is known to exist and it has not been loaded. - - ``` - let post = comment.post; - - ``` - - @method belongsTo - @public - @static - @for @ember-data/model - @param {String} modelName (optional) type of the relationship - @param {Object} options (optional) a hash of options - @return {Ember.computed} relationship -*/ -function belongsTo(modelName, options) { - let opts = options; - let userEnteredModelName = modelName; - if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { - if (typeof modelName !== 'string' || !modelName.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 modelName === 'object') { - opts = modelName; - userEnteredModelName = undefined; - } else { - opts = options; - userEnteredModelName = modelName; - } - - assert( - 'The first argument to belongsTo must be a string representing a model type key, not an instance of ' + - typeof userEnteredModelName + - ". E.g., to define a relation to the Person model, use belongsTo('person')", - typeof userEnteredModelName === 'string' || typeof userEnteredModelName === 'undefined' - ); - } - } - - if (DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC) { - if (!opts || typeof opts.async !== 'boolean') { - opts = opts || {}; - if (!('async' in opts)) { - opts.async = true; - } - 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) - ); - } - - let meta = { - type: normalizeType(userEnteredModelName), - isRelationship: true, - options: opts, - kind: 'belongsTo', - name: 'Belongs To', - key: null, - }; - - return computed({ - get(key) { - // this is a legacy behavior we may not carry into a new model setup - // it's better to error on disconnected records so users find errors - // in their logic. - if (this.isDestroying || this.isDestroyed) { - return null; - } - const support = lookupLegacySupport(this); - - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` - ); - } - if (Object.prototype.hasOwnProperty.call(opts, 'serialize')) { - warn( - `You provided a serialize option on the "${key}" property in the "${support.identifier.type}" class, this belongs in the serializer. See Serializer and it's implementations https://api.emberjs.com/ember-data/release/classes/Serializer`, - false, - { - id: 'ds.model.serialize-option-in-belongs-to', - } - ); - } - - if (Object.prototype.hasOwnProperty.call(opts, 'embedded')) { - warn( - `You provided an embedded option on the "${key}" property in the "${support.identifier.type}" class, this belongs in the serializer. See EmbeddedRecordsMixin https://api.emberjs.com/ember-data/release/classes/EmbeddedRecordsMixin`, - false, - { - id: 'ds.model.embedded-option-in-belongs-to', - } - ); - } - } - - return support.getBelongsTo(key); - }, - set(key, value) { - const support = lookupLegacySupport(this); - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` - ); - } - } - this.store._join(() => { - support.setDirtyBelongsTo(key, value); - }); - - return support.getBelongsTo(key); - }, - }).meta(meta); -} - -export default computedMacroWithOptionalParams(belongsTo); diff --git a/packages/model/src/-private/belongs-to.ts b/packages/model/src/-private/belongs-to.ts new file mode 100644 index 00000000000..95bbacb4889 --- /dev/null +++ b/packages/model/src/-private/belongs-to.ts @@ -0,0 +1,403 @@ +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'; +import { RecordStore } from '@warp-drive/core-types/symbols'; + +import { lookupLegacySupport } from './legacy-relationships-support'; +import type { MinimalLegacyRecord } from './model-methods'; +import { isElementDescriptor } from './util'; +/** + @module @ember-data/model +*/ + +export type IsUnknown = unknown extends T ? true : false; + +export type RelationshipOptions = { + async: Async; + inverse: null | (IsUnknown extends true ? string : keyof NoNull & string); + polymorphic?: boolean; + as?: string; + resetOnRemoteUpdate?: boolean; +}; + +export type NoNull = Exclude; +// type BelongsToDecoratorObject = { +// get: () => getT; +// // set: (value: Awaited) => void; +// set: (value: getT) => void; +// // init: () => getT; +// }; +// 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 { + 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: normalizeType(type), + options: opts, + kind: 'belongsTo', + name: '', + }; + + return computed({ + get(this: R, key: string) { + // this is a legacy behavior we may not carry into a new model setup + // it's better to error on disconnected records so users find errors + // in their logic. + if (this.isDestroying || this.isDestroyed) { + return null; + } + const support = lookupLegacySupport(this); + + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` + ); + } + if (Object.prototype.hasOwnProperty.call(options, 'serialize')) { + warn( + `You provided a serialize option on the "${key}" property in the "${support.identifier.type}" class, this belongs in the serializer. See Serializer and it's implementations https://api.emberjs.com/ember-data/release/classes/Serializer`, + false, + { + id: 'ds.model.serialize-option-in-belongs-to', + } + ); + } + + if (Object.prototype.hasOwnProperty.call(options, 'embedded')) { + warn( + `You provided an embedded option on the "${key}" property in the "${support.identifier.type}" class, this belongs in the serializer. See EmbeddedRecordsMixin https://api.emberjs.com/ember-data/release/classes/EmbeddedRecordsMixin`, + false, + { + id: 'ds.model.embedded-option-in-belongs-to', + } + ); + } + } + + return support.getBelongsTo(key); + }, + set(this: R, key: string, value: unknown) { + const support = lookupLegacySupport(this); + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your belongsTo on ${this.constructor.toString()}` + ); + } + } + this[RecordStore]._join(() => { + support.setDirtyBelongsTo(key, value); + }); + + return support.getBelongsTo(key); + }, + }).meta(meta) as RelationshipDecorator; +} + +/** + `belongsTo` is used to define One-To-One and One-To-Many, and One-To-None + relationships on a [Model](/ember-data/release/classes/Model). + + `belongsTo` takes a configuration hash as a second parameter, currently + supported options are: + + - `async`: (*required*) A boolean value used to declare whether this is a sync (false) or async (true) relationship. + - `inverse`: (*required*) A string used to identify the inverse property on a related model, or `null`. + - `polymorphic`: (*optional*) A boolean value to mark the relationship as polymorphic + - `as`: (*optional*) A string used to declare the abstract type "this" record satisfies for polymorphism. + + ### Examples + + To declare a **one-to-many** (or many-to-many) relationship, use + `belongsTo` in combination with `hasMany`: + + ```js + // app/models/comment.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Comment extends Model { + @belongsTo('post', { async: false, inverse: 'comments' }) post; + } + + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('comment', { async: false, inverse: 'post' }) comments; + } + ``` + + To declare a **one-to-one** relationship with managed inverses, use `belongsTo` for both sides: + + ```js + // app/models/author.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Author extends Model { + @belongsTo('address', { async: true, inverse: 'owner' }) address; + } + + // app/models/address.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Address extends Model { + @belongsTo('author', { async: true, inverse: 'address' }) owner; + } + ``` + + To declare a **one-to-one** relationship without managed inverses, use `belongsTo` for both sides + with `null` as the inverse: + + ```js + // app/models/author.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Author extends Model { + @belongsTo('address', { async: true, inverse: null }) address; + } + + // app/models/address.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Address extends Model { + @belongsTo('author', { async: true, inverse: null }) owner; + } + ``` + + To declare a one-to-none relationship between two models, use + `belongsTo` with inverse set to `null` on just one side:: + + ```js + // app/models/person.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Person extends Model { + @belongsTo('person', { async: false, inverse: null }) bestFriend; + } + ``` + + #### Sync vs Async Relationships + + EmberData fulfills relationships using resource data available in + the cache. + + Sync relationships point directly to the known related resources. + + When a relationship is declared as async, if any of the known related + resources have not been loaded, they will be fetched. The property + on the record when accessed provides a promise that resolves once + all resources are loaded. + + Async relationships may take advantage of links. On access, if the related + link has not been loaded, or if any known resources are not available in + the cache, the fresh state will be fetched using the link. + + In contrast to async relationship, accessing a sync relationship + will error on access when any of the known related resources have + not been loaded. + + If you are using `links` with sync relationships, you have to use + the BelongsTo reference API to fetch or refresh related resources + that aren't loaded. For instance, for a `bestFriend` relationship: + + ```js + person.belongsTo('bestFriend').reload(); + ``` + + #### Polymorphic Relationships + + To declare a polymorphic relationship, use `hasMany` with the `polymorphic` + option set to `true`: + + ```js + // app/models/comment.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Comment extends Model { + @belongsTo('commentable', { async: false, inverse: 'comments', polymorphic: true }) parent; + } + ``` + + `'commentable'` here is referred to as the "abstract type" for the polymorphic + relationship. + + Polymorphic relationships with `inverse: null` will accept any type of record as their content. + Polymorphic relationships with `inverse` set to a string will only accept records with a matching + inverse relationships declaring itself as satisfying the abstract type. + + Below, 'as' is used to declare the that 'post' record satisfies the abstract type 'commentable' + for this relationship. + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('comment', { async: false, inverse: 'parent', as: 'commentable' }) comments; + } + ``` + + Note: every Model that declares an inverse to a polymorphic relationship must + declare itself exactly the same. This is because polymorphism is based on structural + traits. + + Polymorphic to polymorphic relationships are supported. Both sides of the relationship + must be declared as polymorphic, and the `as` option must be used to declare the abstract + type each record satisfies on both sides. + + @method belongsTo + @public + @static + @for @ember-data/model + @param {string} type (optional) the name of the related resource + @param {object} options (optional) a hash of options + @return {PropertyDescriptor} relationship +*/ + +export function belongsTo(): never; +export function belongsTo(type: string): never; +export function belongsTo( + type: TypeFromInstance>, + options: RelationshipOptions +): RelationshipDecorator; +// export function belongsTo, T extends Awaited = Awaited>( +// type: TypeFromInstance>, +// options: RelationshipOptions +// ): RelationshipDecorator; +export function belongsTo(type: string, options: RelationshipOptions): RelationshipDecorator; +export function belongsTo( + type?: TypeFromInstance>, + options?: RelationshipOptions +): RelationshipDecorator { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + assert( + `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!); + } +} diff --git a/packages/model/src/-private/belongs-to.type-test.ts b/packages/model/src/-private/belongs-to.type-test.ts new file mode 100644 index 00000000000..9c66f43f19e --- /dev/null +++ b/packages/model/src/-private/belongs-to.type-test.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { expectTypeOf } from 'expect-type'; + +import type { Type } from '@warp-drive/core-types/symbols'; + +import type { RelationshipDecorator, RelationshipOptions } from './belongs-to'; +import { belongsTo } from './belongs-to'; + +// ------------------------------ +// 💚 +// ============================== +// Type Tests +// ============================== +// 🐹 +// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ + +type User = { + [Type]: 'user'; + friends: User[]; +}; + +expectTypeOf<{ async: false; inverse: null }>().toMatchTypeOf>(); +expectTypeOf<{ async: true; inverse: 'comments' }>().toMatchTypeOf>(); +expectTypeOf<{ async: false; inverse: 'friends' }>().toMatchTypeOf>(); +expectTypeOf<{ async: true; inverse: 'friends' }>().toMatchTypeOf>(); +expectTypeOf<{ async: false; inverse: null }>().toMatchTypeOf>(); +expectTypeOf<{ async: false; inverse: 'notfriends' }>().not.toMatchTypeOf>(); +expectTypeOf<{ async: false; inverse: 'friends' }>().not.toMatchTypeOf>(); +expectTypeOf<{ async: true; inverse: 'friends' }>().not.toMatchTypeOf>(); + +expectTypeOf(belongsTo()).toBeNever; +expectTypeOf(belongsTo('user')).toBeNever; +expectTypeOf(belongsTo('user', { async: false, inverse: null })).toMatchTypeOf>(); +expectTypeOf(belongsTo('user', { async: false, inverse: 'comments' })).toMatchTypeOf>(); + +type CompanyType = { + ceo: User; +}; +class Company { + // to confirm we can be called as a decorator + @belongsTo('user', { async: false, inverse: null }) declare ceo: User; +} +expectTypeOf().toMatchTypeOf(); diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.js b/packages/model/src/-private/debug/assert-polymorphic-type.js deleted file mode 100644 index 4823de17d00..00000000000 --- a/packages/model/src/-private/debug/assert-polymorphic-type.js +++ /dev/null @@ -1,76 +0,0 @@ -import { assert } from '@ember/debug'; -import { DEBUG } from '@ember-data/env'; - -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; - -/* - Assert that `addedRecord` has a valid type so it can be added to the - relationship of the `record`. - - The assert basically checks if the `addedRecord` can be added to the - relationship (specified via `relationshipMeta`) of the `record`. - - This utility should only be used internally, as both record parameters must - be stable record identifiers and the `relationshipMeta` needs to be the meta - information about the relationship, retrieved via - `record.relationshipFor(key)`. -*/ -let assertPolymorphicType; - -if (DEBUG) { - let checkPolymorphic = function checkPolymorphic(modelClass, addedModelClass) { - if (modelClass.__isMixin) { - return ( - modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || - // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` - modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) - ); - } - - return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); - }; - - assertPolymorphicType = function assertPolymorphicType(parentIdentifier, parentDefinition, addedIdentifier, store) { - let asserted = false; - - if (parentDefinition.inverseIsImplicit) { - return; - } - if (parentDefinition.isPolymorphic) { - let meta = store.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; - 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) { - 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._store ? store._store : store; // allow usage with storeWrapper - let addedModelName = addedIdentifier.type; - let parentModelName = parentIdentifier.type; - let key = parentDefinition.key; - let relationshipModelName = parentDefinition.type; - let relationshipClass = store.modelFor(relationshipModelName); - let addedClass = store.modelFor(addedModelName); - - let 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.`; - let isPolymorphic = checkPolymorphic(relationshipClass, addedClass); - - assert(assertionMessage, isPolymorphic); - } - } - }; -} - -export { assertPolymorphicType }; diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.ts b/packages/model/src/-private/debug/assert-polymorphic-type.ts new file mode 100644 index 00000000000..bf9f57b79ee --- /dev/null +++ b/packages/model/src/-private/debug/assert-polymorphic-type.ts @@ -0,0 +1,123 @@ +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 + relationship of the `record`. + + The assert basically checks if the `addedRecord` can be added to the + relationship (specified via `relationshipMeta`) of the `record`. + + This utility should only be used internally, as both record parameters must + be stable record identifiers and the `relationshipMeta` needs to be the meta + information about the relationship, retrieved via + `record.relationshipFor(key)`. +*/ +let assertPolymorphicType: ( + parentIdentifier: StableRecordIdentifier, + parentDefinition: UpgradedMeta, + addedIdentifier: StableRecordIdentifier, + store: Store +) => 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, + parentDefinition: UpgradedMeta, + addedIdentifier: StableRecordIdentifier, + store: Store + ) { + if (parentDefinition.inverseIsImplicit) { + return; + } + let asserted = false; + if (parentDefinition.isPolymorphic) { + const meta = store.schema.fields(addedIdentifier)?.get(parentDefinition.inverseKey); + assert( + `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); + } + } + } + }; +} + +export { assertPolymorphicType }; diff --git a/packages/model/src/-private/deprecated-promise-proxy.ts b/packages/model/src/-private/deprecated-promise-proxy.ts index d95dcf2feda..0adb72058ae 100644 --- a/packages/model/src/-private/deprecated-promise-proxy.ts +++ b/packages/model/src/-private/deprecated-promise-proxy.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { get } from '@ember/object'; -import { DEBUG } from '@ember-data/env'; +import { DEBUG } from '@warp-drive/build-config/env'; import { PromiseObject } from './promise-proxy-base'; @@ -69,5 +69,5 @@ export function deprecatedPromiseObject(promise: Promise): PromiseObject; } diff --git a/packages/model/src/-private/diff-array.ts b/packages/model/src/-private/diff-array.ts deleted file mode 100644 index fde8492d3d9..00000000000 --- a/packages/model/src/-private/diff-array.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - @module @ember-data/model -*/ - -export interface ArrayDiffResult { - firstChangeIndex: number | null; - removedCount: number; - addedCount: number; -} - -/** - @method diffArray - @internal - @param {Array} oldArray the old array - @param {Array} newArray the new array - @return {hash} { - firstChangeIndex: , // null if no change - addedCount: , // 0 if no change - removedCount: // 0 if no change - } -*/ -export default function diffArray(oldArray: unknown[], newArray: unknown[]): ArrayDiffResult { - const oldLength = oldArray.length; - const newLength = newArray.length; - - const shortestLength = Math.min(oldLength, newLength); - let firstChangeIndex: number | null = null; // null signifies no changes - - // find the first change - for (let i = 0; i < shortestLength; i++) { - // compare each item in the array - if (oldArray[i] !== newArray[i]) { - firstChangeIndex = i; - break; - } - } - - if (firstChangeIndex === null && newLength !== oldLength) { - // no change found in the overlapping block - // and array lengths differ, - // so change starts at end of overlap - firstChangeIndex = shortestLength; - } - - let addedCount = 0; - let removedCount = 0; - if (firstChangeIndex !== null) { - // we found a change, find the end of the change - let unchangedEndBlockLength = shortestLength - firstChangeIndex; - // walk back from the end of both arrays until we find a change - for (let i = 1; i <= shortestLength; i++) { - // compare each item in the array - if (oldArray[oldLength - i] !== newArray[newLength - i]) { - unchangedEndBlockLength = i - 1; - break; - } - } - addedCount = newLength - unchangedEndBlockLength - firstChangeIndex; - removedCount = oldLength - unchangedEndBlockLength - firstChangeIndex; - } - - return { - firstChangeIndex, - addedCount, - removedCount, - }; -} diff --git a/packages/model/src/-private/errors.ts b/packages/model/src/-private/errors.ts index 4b2d9ad056d..44d00e7c654 100644 --- a/packages/model/src/-private/errors.ts +++ b/packages/model/src/-private/errors.ts @@ -1,5 +1,4 @@ -import { A } from '@ember/array'; -import type NativeArray from '@ember/array/-private/native-array'; +import { A, type NativeArray } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; import { computed, get } from '@ember/object'; import { mapBy, not } from '@ember/object/computed'; @@ -13,7 +12,7 @@ type ValidationError = { /** @module @ember-data/model */ -interface ArrayProxyWithCustomOverrides extends Omit, 'clear' | 'content'> { +interface ArrayProxyWithCustomOverrides extends Omit, 'clear' | 'content'> { // Omit causes `content` to be merged with the class def for ArrayProxy // which then causes it to be seen as a property, disallowing defining it // as an accessor. This restores our ability to define it as an accessor. @@ -25,7 +24,7 @@ interface ArrayProxyWithCustomOverrides extends Omit, // we force the type here to our own construct because mixin and extend patterns // lose generic signatures. We also do this because we need to Omit `clear` from // the type of ArrayProxy as we override it's signature. -const ArrayProxyWithCustomOverrides = ArrayProxy as unknown as new () => ArrayProxyWithCustomOverrides; +const ArrayProxyWithCustomOverrides = ArrayProxy as unknown as new () => ArrayProxyWithCustomOverrides; /** Holds validation errors for a given record, organized by attribute names. @@ -102,7 +101,7 @@ const ArrayProxyWithCustomOverrides = ArrayProxy as unknown as new () @public @extends Ember.ArrayProxy */ -export default class Errors extends ArrayProxyWithCustomOverrides { +export class Errors extends ArrayProxyWithCustomOverrides { declare __record: { currentState: RecordState }; /** @property errorsByAttributeName @@ -134,7 +133,7 @@ export default class Errors extends ArrayProxyWithCustomOverrides { - let map = this.errorsByAttributeName; + const map = this.errorsByAttributeName; let errors = map.get(attribute); @@ -186,7 +185,7 @@ export default class Errors extends ArrayProxyWithCustomOverrides { - - }); - - ``` - - If you are using `links` with sync relationships, you have to use - `ref.reload` to fetch the resources. - - @method hasMany - @public - @static - @for @ember-data/model - @param {String} type (optional) type of the relationship - @param {Object} options (optional) a hash of options - @return {Ember.computed} relationship -*/ -function hasMany(type, options) { - 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; - } - - 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)) { - 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 - // serialization. Note that `key` is populated lazily - // the first time the CP is called. - let meta = { - type: normalizeType(type), - options, - isRelationship: true, - kind: 'hasMany', - name: 'Has Many', - key: null, - }; - - return computed({ - get(key) { - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your hasMany on ${this.constructor.toString()}` - ); - } - } - if (this.isDestroying || this.isDestroyed) { - return A(); - } - return lookupLegacySupport(this).getHasMany(key); - }, - set(key, records) { - if (DEBUG) { - if (['currentState'].indexOf(key) !== -1) { - throw new Error( - `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your hasMany on ${this.constructor.toString()}` - ); - } - } - const support = lookupLegacySupport(this); - const manyArray = support.getManyArray(key); - assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); - this.store._join(() => { - manyArray.splice(0, manyArray.length, ...records); - }); - - return support.getHasMany(key); - }, - }).meta(meta); -} - -export default computedMacroWithOptionalParams(hasMany); diff --git a/packages/model/src/-private/has-many.ts b/packages/model/src/-private/has-many.ts new file mode 100644 index 00000000000..305ae73431c --- /dev/null +++ b/packages/model/src/-private/has-many.ts @@ -0,0 +1,353 @@ +/** + @module @ember-data/model +*/ +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, + 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'; +import { RecordStore } from '@warp-drive/core-types/symbols'; + +import type { NoNull, RelationshipDecorator, RelationshipOptions } from './belongs-to'; +import { lookupLegacySupport } from './legacy-relationships-support'; +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}'.`, + /* 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 _hasMany( + type: string, + options: RelationshipOptions +): RelationshipDecorator { + 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 + // serialization. Note that `key` is populated lazily + // the first time the CP is called. + const meta = { + type: normalizeType(type), + options, + kind: 'hasMany', + name: '', + }; + + return computed({ + get(this: R, key: string) { + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your hasMany on ${this.constructor.toString()}` + ); + } + } + if (this.isDestroying || this.isDestroyed) { + return []; + } + return lookupLegacySupport(this).getHasMany(key); + }, + set(this: R, key: string, records: T[]) { + if (DEBUG) { + if (['currentState'].includes(key)) { + throw new Error( + `'${key}' is a reserved property name on instances of classes extending Model. Please choose a different property name for your hasMany on ${this.constructor.toString()}` + ); + } + } + const support = lookupLegacySupport(this); + const manyArray = support.getManyArray(key); + assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); + this[RecordStore]._join(() => { + manyArray.splice(0, manyArray.length, ...records); + }); + + return support.getHasMany(key); + }, + }).meta(meta); +} + +/** + `hasMany` is used to define Many-To-One and Many-To-Many, and Many-To-None + relationships on a [Model](/ember-data/release/classes/Model). + + `hasMany` takes a configuration hash as a second parameter, currently + supported options are: + + - `async`: (*required*) A boolean value used to declare whether this is a sync (false) or async (true) relationship. + - `inverse`: (*required*) A string used to identify the inverse property on a related model, or `null`. + - `polymorphic`: (*optional*) A boolean value to mark the relationship as polymorphic + - `as`: (*optional*) A string used to declare the abstract type "this" record satisfies for polymorphism. + + ### Examples + + To declare a **many-to-one** (or one-to-many) relationship, use + `belongsTo` in combination with `hasMany`: + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('comment', { async: false, inverse: 'post' }) comments; + } + + + // app/models/comment.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Comment extends Model { + @belongsTo('post', { async: false, inverse: 'comments' }) post; + } + ``` + + To declare a **many-to-many** relationship with managed inverses, use `hasMany` for both sides: + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('tag', { async: true, inverse: 'posts' }) tags; + } + + // app/models/tag.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Tag extends Model { + @hasMany('post', { async: true, inverse: 'tags' }) posts; + } + ``` + + To declare a **many-to-many** relationship without managed inverses, use `hasMany` for both sides + with `null` as the inverse: + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('tag', { async: true, inverse: null }) tags; + } + + // app/models/tag.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Tag extends Model { + @hasMany('post', { async: true, inverse: null }) posts; + } + ``` + + To declare a many-to-none relationship between two models, use + `hasMany` with inverse set to `null` on just one side:: + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('category', { async: true, inverse: null }) categories; + } + ``` + + #### Sync vs Async Relationships + + EmberData fulfills relationships using resource data available in + the cache. + + Sync relationships point directly to the known related resources. + + When a relationship is declared as async, if any of the known related + resources have not been loaded, they will be fetched. The property + on the record when accessed provides a promise that resolves once + all resources are loaded. + + Async relationships may take advantage of links. On access, if the related + link has not been loaded, or if any known resources are not available in + the cache, the fresh state will be fetched using the link. + + In contrast to async relationship, accessing a sync relationship + will error on access when any of the known related resources have + not been loaded. + + If you are using `links` with sync relationships, you have to use + the HasMany reference API to fetch or refresh related resources + that aren't loaded. For instance, for a `comments` relationship: + + ```js + post.hasMany('comments').reload(); + ``` + + #### Polymorphic Relationships + + To declare a polymorphic relationship, use `hasMany` with the `polymorphic` + option set to `true`: + + ```js + // app/models/comment.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class Comment extends Model { + @belongsTo('commentable', { async: false, inverse: 'comments', polymorphic: true }) parent; + } + ``` + + `'commentable'` here is referred to as the "abstract type" for the polymorphic + relationship. + + Polymorphic relationships with `inverse: null` will accept any type of record as their content. + Polymorphic relationships with `inverse` set to a string will only accept records with a matching + inverse relationships declaring itself as satisfying the abstract type. + + Below, 'as' is used to declare the that 'post' record satisfies the abstract type 'commentable' + for this relationship. + + ```js + // app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class Post extends Model { + @hasMany('comment', { async: false, inverse: 'parent', as: 'commentable' }) comments; + } + ``` + + Note: every Model that declares an inverse to a polymorphic relationship must + declare itself exactly the same. This is because polymorphism is based on structural + traits. + + Polymorphic to polymorphic relationships are supported. Both sides of the relationship + must be declared as polymorphic, and the `as` option must be used to declare the abstract + type each record satisfies on both sides. + + @method hasMany + @public + @static + @for @ember-data/model + @param {string} type (optional) the name of the related resource + @param {object} options (optional) a hash of options + @return {PropertyDescriptor} relationship +*/ +export function hasMany(): never; +export function hasMany(type: string): never; +export function hasMany( + type: TypeFromInstance>, + options: RelationshipOptions +): RelationshipDecorator; +// export function hasMany, T extends Awaited = Awaited>( +// type: TypeFromInstance>, +// options: RelationshipOptions +// ): RelationshipDecorator; +export function hasMany(type: string, options: RelationshipOptions): RelationshipDecorator; +export function hasMany( + type?: TypeFromInstance>, + options?: RelationshipOptions +): RelationshipDecorator { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + assert( + `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!); + } +} diff --git a/packages/model/src/-private/has-many.type-test.ts b/packages/model/src/-private/has-many.type-test.ts new file mode 100644 index 00000000000..70b7aa3a651 --- /dev/null +++ b/packages/model/src/-private/has-many.type-test.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { expectTypeOf } from 'expect-type'; + +import type { Type } from '@warp-drive/core-types/symbols'; + +import type { RelationshipDecorator } from './belongs-to'; +import { hasMany } from './has-many'; + +// ------------------------------ +// 💚 +// ============================== +// Type Tests +// ============================== +// 🐹 +// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ + +type User = { + [Type]: 'user'; + friends: User[]; +}; + +expectTypeOf(hasMany()).toBeNever; +expectTypeOf(hasMany('user')).toBeNever; +expectTypeOf(hasMany('user', { async: false, inverse: null })).toMatchTypeOf>(); +expectTypeOf(hasMany('user', { async: false, inverse: 'comments' })).toMatchTypeOf>(); + +type CompanyType = { + executives: User[]; +}; +class Company { + // to confirm we can be called as a decorator + @hasMany('user', { async: false, inverse: null }) declare executives: User[]; +} +expectTypeOf().toMatchTypeOf(); diff --git a/packages/model/src/-private/hooks.ts b/packages/model/src/-private/hooks.ts new file mode 100644 index 00000000000..a4d765cdd16 --- /dev/null +++ b/packages/model/src/-private/hooks.ts @@ -0,0 +1,82 @@ +import { getOwner, setOwner } from '@ember/application'; + +import { setCacheFor, setRecordIdentifier, type Store, StoreMap } from '@ember-data/store/-private'; +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'; +import type { TypeFromInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; + +import type { Model, ModelStore } from './model'; +import { getModelFactory } from './schema-provider'; +import { normalizeModelName } from './util'; + +function recast(context: Store): asserts context is ModelStore {} + +export function instantiateRecord( + this: Store, + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } +): Model { + const type = identifier.type; + + recast(this); + + const cache = this.cache; + // TODO deprecate allowing unknown args setting + const createOptions = { + _createProps: createRecordArgs, + // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for + _secretInit: { + identifier, + cache, + store: this, + cb: secretInit, + }, + }; + + // ensure that `getOwner(this)` works inside a model instance + setOwner(createOptions, getOwner(this)!); + const factory = getModelFactory(this, type); + + assert(`No model was found for '${type}'`, factory); + return factory.class.create(createOptions); +} + +export function teardownRecord(record: Model): void { + assert( + `expected to receive an instance of Model from @ember-data/model. If using a custom model make sure you implement teardownRecord`, + 'destroy' in record + ); + record.destroy(); +} + +export function modelFor(type: TypeFromInstance): typeof Model | void; +export function modelFor(type: string): typeof Model | void; +export function modelFor(this: Store, modelName: TypeFromInstanceOrString): typeof Model | void { + assert( + `Attempted to call store.modelFor(), but the store instance has already been destroyed.`, + !this.isDestroyed && !this.isDestroying + ); + assert(`You need to pass a model name to the store's modelFor method`, modelName); + assert( + `Please pass a proper model name to the store's modelFor method`, + typeof modelName === 'string' && modelName.length + ); + recast(this); + + const type = normalizeModelName(modelName); + const maybeFactory = getModelFactory(this, type); + const klass = maybeFactory && maybeFactory.class ? maybeFactory.class : null; + + const ignoreType = !klass || !klass.isModel || this._forceShim; + if (!ignoreType) { + return klass; + } + assert(`No model was found for '${type}' and no schema handles the type`, this.schema.hasResource({ type })); +} + +function secretInit(record: Model, cache: Cache, identifier: StableRecordIdentifier, store: Store): void { + setRecordIdentifier(record, identifier); + StoreMap.set(record, store); + setCacheFor(record, cache); +} diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index c0314a4e66f..ef6fe4831f4 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -1,16 +1,11 @@ -import { assert, deprecate } from '@ember/debug'; - -import { importSync } from '@embroider/macros'; - -import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import type { UpgradedMeta } from '@ember-data/graph/-private/graph/-edge-definition'; -import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/index'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import { HAS_JSON_API_PACKAGE } from '@ember-data/packages'; +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 { LiveArray } from '@ember-data/store/-private'; import { fastPush, isStableIdentifier, @@ -19,57 +14,89 @@ import { SOURCE, storeFor, } from '@ember-data/store/-private'; -import type { NonSingletonCacheManager } from '@ember-data/store/-private/managers/cache-manager'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiRelationship } from '@ember-data/types/q/record-data-json-api'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; - -import RelatedCollection from './many-array'; +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'; +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; +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 type { + CollectionResourceRelationship, + InnerRelationshipDocument, + SingleResourceRelationship, +} from '@warp-drive/core-types/spec/json-api-raw'; + +import { RelatedCollection as ManyArray } from './many-array'; +import type { MinimalLegacyRecord } from './model-methods'; import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to'; -import PromiseBelongsTo from './promise-belongs-to'; +import { PromiseBelongsTo } from './promise-belongs-to'; import type { HasManyProxyCreateArgs } from './promise-many-array'; -import PromiseManyArray from './promise-many-array'; +import { PromiseManyArray } from './promise-many-array'; import BelongsToReference from './references/belongs-to'; import HasManyReference from './references/has-many'; -type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; +type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; + +export const LEGACY_SUPPORT = getOrSetGlobal( + 'LEGACY_SUPPORT', + new Map() +); + +export function lookupLegacySupport(record: MinimalLegacyRecord): LegacySupport { + const identifier = recordIdentifierFor(record); + assert(`Expected a record`, identifier); + let support = LEGACY_SUPPORT.get(identifier); + + if (!support) { + assert(`Memory Leak Detected`, !record.isDestroyed && !record.isDestroying); + support = new LegacySupport(record); + LEGACY_SUPPORT.set(identifier, support); + LEGACY_SUPPORT.set(record, support); + } + + return support; +} export class LegacySupport { - declare record: DSModel; + declare record: MinimalLegacyRecord; declare store: Store; + declare graph: Graph; declare cache: Cache; - declare references: Dict; + declare references: Record; declare identifier: StableRecordIdentifier; - declare _manyArrayCache: Record; - declare _relationshipPromisesCache: Record>; - declare _relationshipProxyCache: Record; + declare _manyArrayCache: Record; + declare _relationshipPromisesCache: Record>; + declare _relationshipProxyCache: Record; declare _pending: Record | undefined>; declare isDestroying: boolean; declare isDestroyed: boolean; - constructor(record: DSModel) { + constructor(record: MinimalLegacyRecord) { this.record = record; this.store = storeFor(record)!; this.identifier = recordIdentifierFor(record); this.cache = peekCache(record); - this._manyArrayCache = Object.create(null) as Record; - this._relationshipPromisesCache = Object.create(null) as Record< - string, - Promise - >; + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { + const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) + .graphFor; + + this.graph = graphFor(this.store); + } + + this._manyArrayCache = Object.create(null) as Record; + this._relationshipPromisesCache = Object.create(null) as Record>; this._relationshipProxyCache = Object.create(null) as Record; this._pending = Object.create(null) as Record>; this.references = Object.create(null) as Record; } - _syncArray(array: RelatedCollection) { + _syncArray(array: LiveArray) { // It’s possible the parent side of the relationship may have been destroyed by this point if (this.isDestroyed || this.isDestroying) { return; @@ -77,7 +104,7 @@ export class LegacySupport { const currentState = array[SOURCE]; const identifier = this.identifier; - let [identifiers, jsonApi] = this._getCurrentState(identifier, array.key); + const [identifiers, jsonApi] = this._getCurrentState(identifier, (array as ManyArray).key); if (jsonApi.meta) { array.meta = jsonApi.meta; @@ -98,9 +125,9 @@ export class LegacySupport { _findBelongsTo( key: string, resource: SingleResourceRelationship, - relationship: BelongsToRelationship, - options?: FindOptions - ): Promise { + relationship: ResourceEdge, + options?: BaseFinderOptions + ): Promise { // TODO @runspired follow up if parent isNew then we should not be attempting load here // TODO @runspired follow up on whether this should be in the relationship requests cache return this._findBelongsToByJsonApiResource(resource, this.identifier, relationship, options).then( @@ -110,39 +137,38 @@ export class LegacySupport { ); } - reloadBelongsTo(key: string, options?: FindOptions): Promise { - let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + reloadBelongsTo(key: string, options?: BaseFinderOptions): Promise { + const loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; - const relationship = graphFor(this.store).get(this.identifier, key); + const relationship = this.graph.get(this.identifier, key); assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship)); - let resource = this.cache.getRelationship(this.identifier, key) as SingleResourceRelationship; + const resource = this.cache.getRelationship(this.identifier, key) as SingleResourceRelationship; relationship.state.hasFailedLoadAttempt = false; relationship.state.shouldForceReload = true; - let promise = this._findBelongsTo(key, resource, relationship, options); + const promise = this._findBelongsTo(key, resource, relationship, options); if (this._relationshipProxyCache[key]) { + // @ts-expect-error return this._updatePromiseProxyFor('belongsTo', key, { promise }); } return promise; } - getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null { + getBelongsTo(key: string, options?: BaseFinderOptions): PromiseBelongsTo | OpaqueRecordInstance | null { const { identifier, cache } = this; - let resource = cache.getRelationship(this.identifier, key) as SingleResourceRelationship; - let relatedIdentifier = resource && resource.data ? resource.data : null; + const resource = cache.getRelationship(this.identifier, key) as SingleResourceRelationship; + const relatedIdentifier = resource && resource.data ? resource.data : null; assert(`Expected a stable identifier`, !relatedIdentifier || isStableIdentifier(relatedIdentifier)); const store = this.store; - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; - const relationship = graphFor(store).get(this.identifier, key); + const relationship = this.graph.get(this.identifier, key); assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship)); - let isAsync = relationship.definition.isAsync; - let _belongsToState: BelongsToProxyMeta = { + const isAsync = relationship.definition.isAsync; + const _belongsToState: BelongsToProxyMeta = { key, store, legacySupport: this, @@ -154,12 +180,12 @@ export class LegacySupport { return this._relationshipProxyCache[key] as PromiseBelongsTo; } - let promise = this._findBelongsTo(key, resource, relationship, options); + const promise = this._findBelongsTo(key, resource, relationship, options); const isLoaded = relatedIdentifier && store._instanceCache.recordIsLoaded(relatedIdentifier); return this._updatePromiseProxyFor('belongsTo', key, { promise, - content: isLoaded ? store._instanceCache.getRecord(relatedIdentifier!) : null, + content: isLoaded ? store._instanceCache.getRecord(relatedIdentifier) : null, _belongsToState, }); } else { @@ -177,7 +203,7 @@ export class LegacySupport { } } - setDirtyBelongsTo(key: string, value: RecordInstance | null) { + setDirtyBelongsTo(key: string, value: OpaqueRecordInstance | null) { return this.cache.mutate( { op: 'replaceRelatedRecord', @@ -190,23 +216,19 @@ export class LegacySupport { ); } - _getCurrentState( + _getCurrentState( identifier: StableRecordIdentifier, field: string - ): [StableRecordIdentifier[], CollectionResourceRelationship] { - let jsonApi = (this.cache as NonSingletonCacheManager).getRelationship( - identifier, - field, - true - ) as CollectionResourceRelationship; + ): [StableRecordIdentifier>[], CollectionRelationship] { + const jsonApi = this.cache.getRelationship(identifier, field) as CollectionRelationship; const cache = this.store._instanceCache; - let identifiers: StableRecordIdentifier[] = []; + const identifiers: StableRecordIdentifier>[] = []; if (jsonApi.data) { for (let i = 0; i < jsonApi.data.length; i++) { - const identifier = jsonApi.data[i]; - assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - if (cache.recordIsLoaded(identifier, true)) { - identifiers.push(identifier); + const relatedIdentifier = jsonApi.data[i] as StableRecordIdentifier>; + assert(`Expected a stable identifier`, isStableIdentifier(relatedIdentifier)); + if (cache.recordIsLoaded(relatedIdentifier, true)) { + identifiers.push(relatedIdentifier); } } } @@ -214,21 +236,19 @@ export class LegacySupport { return [identifiers, jsonApi]; } - getManyArray(key: string, definition?: UpgradedMeta): RelatedCollection { - if (HAS_JSON_API_PACKAGE) { - let manyArray: RelatedCollection | undefined = this._manyArrayCache[key]; + getManyArray(key: string, definition?: UpgradedMeta): ManyArray { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { + let manyArray: ManyArray | undefined = this._manyArrayCache[key] as ManyArray | undefined; if (!definition) { - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - definition = graphFor(this.store).get(this.identifier, key).definition; + definition = this.graph.get(this.identifier, key).definition; } if (!manyArray) { - const [identifiers, doc] = this._getCurrentState(this.identifier, key); + const [identifiers, doc] = this._getCurrentState(this.identifier, key); - manyArray = new RelatedCollection({ + manyArray = new ManyArray({ store: this.store, - type: definition.type, + type: definition.type as TypeFromInstanceOrString, identifier: this.identifier, cache: this.cache, identifiers, @@ -252,17 +272,17 @@ export class LegacySupport { fetchAsyncHasMany( key: string, - relationship: ManyRelationship, - manyArray: RelatedCollection, - options?: FindOptions - ): Promise { - if (HAS_JSON_API_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + relationship: CollectionEdge, + manyArray: ManyArray, + options?: BaseFinderOptions + ): Promise { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } - const jsonApi = this.cache.getRelationship(this.identifier, key) as CollectionResourceRelationship; + const jsonApi = this.cache.getRelationship(this.identifier, key) as CollectionRelationship; const promise = this._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options); if (!promise) { @@ -280,45 +300,41 @@ export class LegacySupport { assert('hasMany only works with the @ember-data/json-api package'); } - reloadHasMany(key: string, options?: FindOptions) { - if (HAS_JSON_API_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key]; + reloadHasMany(key: string, options?: BaseFinderOptions): Promise> | PromiseManyArray { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { + const loadingPromise = this._relationshipPromisesCache[key]; if (loadingPromise) { - return loadingPromise; + return loadingPromise as Promise>; } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + const relationship = this.graph.get(this.identifier, key) as CollectionEdge; const { definition, state } = relationship; state.hasFailedLoadAttempt = false; state.shouldForceReload = true; - let manyArray = this.getManyArray(key, definition); - let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); + const manyArray = this.getManyArray(key, definition); + const promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); if (this._relationshipProxyCache[key]) { - return this._updatePromiseProxyFor('hasMany', key, { promise }); + return this._updatePromiseProxyFor('hasMany', key, { promise }) as PromiseManyArray; } - return promise; + return promise as Promise>; } assert(`hasMany only works with the @ember-data/json-api package`); } - getHasMany(key: string, options?: FindOptions): PromiseManyArray | RelatedCollection { - if (HAS_JSON_API_PACKAGE) { - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + getHasMany(key: string, options?: BaseFinderOptions): PromiseManyArray | ManyArray { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { + const relationship = this.graph.get(this.identifier, key) as CollectionEdge; const { definition, state } = relationship; - let manyArray = this.getManyArray(key, definition); + const manyArray = this.getManyArray(key, definition); if (definition.isAsync) { if (state.hasFailedLoadAttempt) { return this._relationshipProxyCache[key] as PromiseManyArray; } - let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); + const promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); return this._updatePromiseProxyFor('hasMany', key, { promise, content: manyArray }); } else { @@ -340,12 +356,12 @@ export class LegacySupport { _updatePromiseProxyFor( kind: 'belongsTo', key: string, - args: { promise: Promise } + args: { promise: Promise } ): PromiseBelongsTo; _updatePromiseProxyFor( kind: 'hasMany' | 'belongsTo', key: string, - args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } + args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } ): PromiseBelongsTo | PromiseManyArray { let promiseProxy = this._relationshipProxyCache[key]; if (kind === 'hasMany') { @@ -374,25 +390,25 @@ export class LegacySupport { return promiseProxy; } - referenceFor(kind: string | null, name: string) { + referenceFor(kind: 'belongsTo', name: string): BelongsToReference; + referenceFor(kind: 'hasMany', name: string): HasManyReference; + referenceFor(kind: 'belongsTo' | 'hasMany', name: string) { let reference = this.references[name]; if (!reference) { - if (!HAS_JSON_API_PACKAGE) { + if (macroCondition(!dependencySatisfies('@ember-data/graph', '*'))) { // TODO @runspired while this feels odd, it is not a regression in capability because we do // not today support references pulling from RecordDatas other than our own // because of the intimate API access involved. This is something we will need to redesign. assert(`snapshot.belongsTo only supported for @ember-data/json-api`); } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const graph = graphFor(this.store); - const relationship = graph.get(this.identifier, name); + const { graph, identifier } = this; + const relationship = graph.get(identifier, name); if (DEBUG) { if (kind) { - let modelName = this.identifier.type; - let actualRelationshipKind = relationship.definition.kind; + const modelName = identifier.type; + const actualRelationshipKind = relationship.definition.kind; assert( `You tried to get the '${name}' relationship on a '${modelName}' via record.${kind}('${name}'), but the relationship is of kind '${actualRelationshipKind}'. Use record.${actualRelationshipKind}('${name}') instead.`, actualRelationshipKind === kind @@ -400,18 +416,12 @@ export class LegacySupport { } } - let relationshipKind = relationship.definition.kind; + const relationshipKind = relationship.definition.kind; if (relationshipKind === 'belongsTo') { - reference = new BelongsToReference( - this.store, - graph, - this.identifier, - relationship as BelongsToRelationship, - name - ); + reference = new BelongsToReference(this.store, graph, identifier, relationship as ResourceEdge, name); } else if (relationshipKind === 'hasMany') { - reference = new HasManyReference(this.store, graph, this.identifier, relationship as ManyRelationship, name); + reference = new HasManyReference(this.store, graph, identifier, relationship as CollectionEdge, name); } this.references[name] = reference; @@ -423,31 +433,34 @@ export class LegacySupport { _findHasManyByJsonApiResource( resource: CollectionResourceRelationship, parentIdentifier: StableRecordIdentifier, - relationship: ManyRelationship, - options: FindOptions = {} + relationship: CollectionEdge, + options: BaseFinderOptions = {} ): Promise | void { - if (HAS_JSON_API_PACKAGE) { + if (macroCondition(dependencySatisfies('@ember-data/graph', '*'))) { if (!resource) { return; } const { definition, state } = relationship; - const adapter = this.store.adapterFor(definition.type); + upgradeStore(this.store); + const adapter = this.store.adapterFor?.(definition.type); const { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state; const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource); const identifiers = resource.data; const shouldFindViaLink = resource.links && resource.links.related && - (typeof adapter.findHasMany === 'function' || typeof identifiers === 'undefined') && + (typeof adapter?.findHasMany === 'function' || typeof identifiers === 'undefined') && (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - const relationshipMeta = this.store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: definition.inverseType })[definition.key]; + const field = this.store.schema.fields({ type: definition.inverseType }).get(definition.key); + assert( + `Expected a hasMany field definition for ${definition.inverseType}.${definition.key}`, + field && field.kind === 'hasMany' + ); const request = { useLink: shouldFindViaLink, - field: relationshipMeta, + field, links: resource.links, meta: resource.meta, options, @@ -463,7 +476,7 @@ export class LegacySupport { op: 'findHasMany', records: identifiers || [], data: request, - cacheOptions: { [Symbol.for('ember-data:skip-cache')]: true }, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, }) as unknown as Promise; } @@ -486,7 +499,7 @@ export class LegacySupport { op: 'findHasMany', records: identifiers, data: request, - cacheOptions: { [Symbol.for('ember-data:skip-cache')]: true }, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, }) as unknown as Promise; } @@ -500,8 +513,8 @@ export class LegacySupport { _findBelongsToByJsonApiResource( resource: SingleResourceRelationship, parentIdentifier: StableRecordIdentifier, - relationship: BelongsToRelationship, - options: FindOptions = {} + relationship: ResourceEdge, + options: BaseFinderOptions = {} ): Promise { if (!resource) { return Promise.resolve(null); @@ -512,26 +525,27 @@ export class LegacySupport { // in order to prevent infinite re-render if the request // fails. if (this._pending[key]) { - return this._pending[key]!; + return this._pending[key]; } const identifier = resource.data ? resource.data : null; assert(`Expected a stable identifier`, !identifier || isStableIdentifier(identifier)); - let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = relationship.state; + const { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = relationship.state; const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource); const shouldFindViaLink = resource.links?.related && (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - const relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier)[ - relationship.definition.key - ]; - assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta); + const field = this.store.schema.fields(this.identifier).get(relationship.definition.key); + assert( + `Attempted to access a belongsTo relationship but no definition exists for it`, + field && field.kind === 'belongsTo' + ); const request = { useLink: shouldFindViaLink, - field: relationshipMeta, + field, links: resource.links, meta: resource.meta, options, @@ -544,14 +558,14 @@ export class LegacySupport { op: 'findBelongsTo', records: identifier ? [identifier] : [], data: request, - cacheOptions: { [Symbol.for('ember-data:skip-cache')]: true }, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, }); this._pending[key] = future .then((doc) => doc.content) .finally(() => { this._pending[key] = undefined; }); - return this._pending[key]!; + return this._pending[key]; } const preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; @@ -581,13 +595,13 @@ export class LegacySupport { op: 'findBelongsTo', records: [identifier], data: request, - cacheOptions: { [Symbol.for('ember-data:skip-cache')]: true }, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, }) .then((doc) => doc.content) .finally(() => { this._pending[key] = undefined; }); - return this._pending[key]!; + return this._pending[key]; } // we were explicitly told we have no data and no links. @@ -598,14 +612,14 @@ export class LegacySupport { destroy() { this.isDestroying = true; - let cache: Dict<{ destroy(): void }> = this._manyArrayCache; - this._manyArrayCache = Object.create(null); + let cache: Record = this._manyArrayCache; + this._manyArrayCache = Object.create(null) as Record; Object.keys(cache).forEach((key) => { cache[key]!.destroy(); }); cache = this._relationshipProxyCache; - this._relationshipProxyCache = Object.create(null); + this._relationshipProxyCache = Object.create(null) as Record; Object.keys(cache).forEach((key) => { const proxy = cache[key]!; if (proxy.destroy) { @@ -614,7 +628,7 @@ export class LegacySupport { }); cache = this.references; - this.references = Object.create(null); + this.references = Object.create(null) as Record; Object.keys(cache).forEach((key) => { cache[key]!.destroy(); }); @@ -625,36 +639,36 @@ export class LegacySupport { function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, - relationship: BelongsToRelationship, + relationship: ResourceEdge, value: StableRecordIdentifier | null -): RecordInstance | null; +): OpaqueRecordInstance | null; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, - relationship: ManyRelationship, - value: RelatedCollection -): RelatedCollection; + relationship: CollectionEdge, + value: ManyArray +): ManyArray; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, - relationship: BelongsToRelationship, + relationship: ResourceEdge, value: null, error: Error ): never; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, - relationship: ManyRelationship, - value: RelatedCollection, + relationship: CollectionEdge, + value: ManyArray, error: Error ): never; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, - relationship: BelongsToRelationship | ManyRelationship, - value: RelatedCollection | StableRecordIdentifier | null, + relationship: ResourceEdge | CollectionEdge, + value: ManyArray | StableRecordIdentifier | null, error?: Error -): RelatedCollection | RecordInstance | null { +): ManyArray | OpaqueRecordInstance | null { delete recordExt._relationshipPromisesCache[key]; relationship.state.shouldForceReload = false; const isHasMany = relationship.definition.kind === 'hasMany'; @@ -662,12 +676,12 @@ function handleCompletedRelationshipRequest( if (isHasMany) { // we don't notify the record property here to avoid refetch // only the many array - (value as RelatedCollection).notify(); + (value as ManyArray).notify(); } if (error) { relationship.state.hasFailedLoadAttempt = true; - let proxy = recordExt._relationshipProxyCache[key]; + const proxy = recordExt._relationshipProxyCache[key]; // belongsTo relationships are sometimes unloaded // when a load fails, in this case we need // to make sure that we aren't proxying @@ -676,7 +690,7 @@ function handleCompletedRelationshipRequest( // for the async reload case there will be no proxy if the ui // has never been accessed if (proxy && !isHasMany) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // @ts-expect-error unsure why this is not resolving the boolean but async belongsTo is weird if (proxy.content && proxy.content.isDestroying) { (proxy as PromiseBelongsTo).set('content', null); } @@ -687,7 +701,7 @@ function handleCompletedRelationshipRequest( } if (isHasMany) { - (value as RelatedCollection).isLoaded = true; + (value as ManyArray).isLoaded = true; } else { recordExt.store.notifications._flush(); } @@ -696,21 +710,19 @@ function handleCompletedRelationshipRequest( // only set to not stale if no error is thrown relationship.state.isStale = false; - return isHasMany || !value - ? (value as RelatedCollection | null) - : recordExt.store.peekRecord(value as StableRecordIdentifier); + return isHasMany || !value ? value : recordExt.store.peekRecord(value as StableRecordIdentifier); } -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { - if (!recordOrPromiseRecord) { +function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordInstance | null) { + if (!record) { return null; } if (DEPRECATE_PROMISE_PROXIES) { - if (isPromiseRecord(recordOrPromiseRecord)) { - let content = recordOrPromiseRecord.content; + 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 @@ -732,25 +744,27 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | } } - return recordIdentifierFor(recordOrPromiseRecord); + return recordIdentifierFor(record); } -function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { - return !!record.then; -} - -function anyUnloaded(store: Store, relationship: ManyRelationship) { - let state = relationship.localState; +function anyUnloaded(store: Store, relationship: CollectionEdge) { + const graph = store._graph; + assert(`Expected a Graph instance to be available`, graph); + const relationshipData = graph.getData( + relationship.identifier, + relationship.definition.key + ) as CollectionRelationship; + const state = relationshipData.data; const cache = store._instanceCache; - const unloaded = state.find((s) => { - let isLoaded = cache.recordIsLoaded(s, true); + const unloaded = state?.find((s) => { + const isLoaded = cache.recordIsLoaded(s, true); return !isLoaded; }); return unloaded || false; } -export function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelationship): boolean { +export function areAllInverseRecordsLoaded(store: Store, resource: InnerRelationshipDocument): boolean { const instanceCache = store._instanceCache; const identifiers = resource.data; @@ -768,8 +782,10 @@ export function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelati return instanceCache.recordIsLoaded(identifiers); } -function isBelongsTo( - relationship: BelongsToRelationship | ImplicitRelationship | ManyRelationship -): relationship is BelongsToRelationship { +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 76b5a5ad1ac..01c73832264 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/model/src/-private/many-array.ts @@ -1,64 +1,52 @@ /** @module @ember-data/store */ -import { assert, deprecate } from '@ember/debug'; +import { deprecate } from '@ember/debug'; -import { DEPRECATE_MANY_ARRAY_DUPLICATES_4_12, DEPRECATE_PROMISE_PROXIES } from '@ember-data/deprecations'; import type Store from '@ember-data/store'; +import type { CreateRecordProperties, NativeProxy } from '@ember-data/store/-private'; import { - IDENTIFIER_ARRAY_TAG, + ARRAY_SIGNAL, isStableIdentifier, + LiveArray, MUTATE, notifyArray, - RecordArray, recordIdentifierFor, SOURCE, } from '@ember-data/store/-private'; -import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class'; -import { IdentifierArrayCreateOptions } from '@ember-data/store/-private/record-arrays/identifier-array'; -import type { CreateRecordProperties } from '@ember-data/store/-private/store-service'; -import { addToTransaction, type Tag } from '@ember-data/tracking/-private'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; - -import { LegacySupport } from './legacy-relationships-support'; - -/* -NOTES ON MANY ARRAY DUPLICATION DEPRECATION APPROACH: - -// 4.6 behavior - -dedupe, no error and no deprecation - -// 4.12 approach - -DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 === true => dedupe, no error -DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 === false => no dedupe, error (same as 6.0) - -// 5.3 approach - -DEPRECATE_MANY_ARRAY_DUPLICATES === true => dedupe, deprecation -DEPRECATE_MANY_ARRAY_DUPLICATES === false => no dedupe, error (same as 6.0) - -// 6.0 approach - -no-dedupe, error -*/ - -export interface ManyArrayCreateArgs { - identifiers: StableRecordIdentifier[]; - type: string; +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, + 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'; +import type { + OpaqueRecordInstance, + TypedRecordInstance, + TypeFromInstance, + TypeFromInstanceOrString, +} from '@warp-drive/core-types/record'; +import type { Links, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { LegacySupport } from './legacy-relationships-support'; + +type IdentifierArrayCreateOptions = ConstructorParameters[0]; + +export interface ManyArrayCreateArgs { + identifiers: StableRecordIdentifier>[]; + type: TypeFromInstanceOrString; store: Store; allowMutation: boolean; manager: LegacySupport; identifier: StableRecordIdentifier; cache: Cache; - meta: Dict | null; + meta: Record | null; links: Links | PaginationLinks | null; key: string; isPolymorphic: boolean; @@ -109,7 +97,7 @@ export interface ManyArrayCreateArgs { @class ManyArray @public */ -export default class RelatedCollection extends RecordArray { +export class RelatedCollection extends LiveArray { declare isAsync: boolean; /** The loading state of this array @@ -164,7 +152,7 @@ export default class RelatedCollection extends RecordArray { @property {Object | null} meta @public */ - declare meta: Dict | null; + declare meta: Record | null; /** * Retrieve the links for this relationship * @@ -174,13 +162,13 @@ export default class RelatedCollection extends RecordArray { declare links: Links | PaginationLinks | null; declare identifier: StableRecordIdentifier; declare cache: Cache; - // @ts-expect-error declare _manager: LegacySupport; declare store: Store; declare key: string; - declare type: ShimModelClass; + declare type: ModelSchema; + declare modelName: T extends TypedRecordInstance ? TypeFromInstance : string; - constructor(options: ManyArrayCreateArgs) { + constructor(options: ManyArrayCreateArgs) { super(options as unknown as IdentifierArrayCreateOptions); this.isLoaded = options.isLoaded || false; this.isAsync = options.isAsync || false; @@ -191,30 +179,39 @@ export default class RelatedCollection extends RecordArray { [MUTATE]( target: StableRecordIdentifier[], - receiver: typeof Proxy, + receiver: typeof NativeProxy, prop: string, args: unknown[], - _TAG: Tag + _SIGNAL: Signal ): unknown { switch (prop) { case 'length 0': { Reflect.set(target, 'length', 0); - mutateReplaceRelatedRecords(this, [], _TAG); + mutateReplaceRelatedRecords(this, [], _SIGNAL); return true; } case 'replace cell': { const [index, prior, value] = args as [number, StableRecordIdentifier, StableRecordIdentifier]; target[index] = value; - mutateReplaceRelatedRecord(this, { value, prior, index }, _TAG); + mutateReplaceRelatedRecord(this, { value, prior, index }, _SIGNAL); return true; } case 'push': { - if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - // dedupe, no error + const newValues = extractIdentifiersFromRecords(args); + + assertNoDuplicates( + this, + target, + (currentState) => currentState.push(...newValues), + `Cannot push duplicates to a hasMany's state.` + ); + + if (DEPRECATE_MANY_ARRAY_DUPLICATES) { + // dedupe const seen = new Set(target); - const unique = new Set(); + const unique = new Set(); - (args as RecordInstance[]).forEach((item) => { + args.forEach((item) => { const identifier = recordIdentifierFor(item); if (!seen.has(identifier)) { seen.add(identifier); @@ -223,27 +220,18 @@ export default class RelatedCollection extends RecordArray { }); const newArgs = Array.from(unique); - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; if (newArgs.length) { - mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(newArgs) }, _TAG); + mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(newArgs) }, _SIGNAL); } return result; } // else, no dedupe, error on duplicates - const newValues = extractIdentifiersFromRecords(args as RecordInstance[]); - - assertNoDuplicates( - this, - target, - (currentState) => currentState.push(...newValues), - `Cannot push duplicates to a hasMany's state.` - ); - - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (newValues.length) { - mutateAddToRelatedRecords(this, { value: newValues }, _TAG); + mutateAddToRelatedRecords(this, { value: newValues }, _SIGNAL); } return result; } @@ -251,18 +239,27 @@ export default class RelatedCollection extends RecordArray { case 'pop': { const result: unknown = Reflect.apply(target[prop], receiver, args); if (result) { - mutateRemoveFromRelatedRecords(this, { value: recordIdentifierFor(result as RecordInstance) }, _TAG); + mutateRemoveFromRelatedRecords(this, { value: recordIdentifierFor(result as OpaqueRecordInstance) }, _SIGNAL); } return result; } case 'unshift': { - if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - // dedupe, no error + const newValues = extractIdentifiersFromRecords(args); + + assertNoDuplicates( + this, + target, + (currentState) => currentState.unshift(...newValues), + `Cannot unshift duplicates to a hasMany's state.` + ); + + if (DEPRECATE_MANY_ARRAY_DUPLICATES) { + // dedupe const seen = new Set(target); - const unique = new Set(); + const unique = new Set(); - (args as RecordInstance[]).forEach((item) => { + args.forEach((item) => { const identifier = recordIdentifierFor(item); if (!seen.has(identifier)) { seen.add(identifier); @@ -274,24 +271,15 @@ export default class RelatedCollection extends RecordArray { const result: unknown = Reflect.apply(target[prop], receiver, newArgs); if (newArgs.length) { - mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(newArgs), index: 0 }, _TAG); + mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(newArgs), index: 0 }, _SIGNAL); } return result; } // else, no dedupe, error on duplicates - const newValues = extractIdentifiersFromRecords(args as RecordInstance[]); - - assertNoDuplicates( - this, - target, - (currentState) => currentState.unshift(...newValues), - `Cannot unshift duplicates to a hasMany's state.` - ); - - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (newValues.length) { - mutateAddToRelatedRecords(this, { value: newValues, index: 0 }, _TAG); + mutateAddToRelatedRecords(this, { value: newValues, index: 0 }, _SIGNAL); } return result; } @@ -302,8 +290,8 @@ export default class RelatedCollection extends RecordArray { if (result) { mutateRemoveFromRelatedRecords( this, - { value: recordIdentifierFor(result as RecordInstance), index: 0 }, - _TAG + { value: recordIdentifierFor(result as OpaqueRecordInstance), index: 0 }, + _SIGNAL ); } return result; @@ -311,28 +299,15 @@ export default class RelatedCollection extends RecordArray { case 'sort': { const result: unknown = Reflect.apply(target[prop], receiver, args); - mutateSortRelatedRecords(this, (result as RecordInstance[]).map(recordIdentifierFor), _TAG); + mutateSortRelatedRecords(this, (result as OpaqueRecordInstance[]).map(recordIdentifierFor), _SIGNAL); return result; } case 'splice': { - const [start, deleteCount, ...adds] = args as [number, number, ...RecordInstance[]]; + const [start, deleteCount, ...adds] = args as [number, number, ...OpaqueRecordInstance[]]; // detect a full replace if (start === 0 && deleteCount === this[SOURCE].length) { - if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - // dedupe, no error - const current = new Set(adds); - const unique = Array.from(current); - const newArgs = ([start, deleteCount] as unknown[]).concat(unique); - - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; - - mutateReplaceRelatedRecords(this, extractIdentifiersFromRecords(unique), _TAG); - return result; - } - - // else, no dedupe, error on duplicates const newValues = extractIdentifiersFromRecords(adds); assertNoDuplicates( @@ -342,18 +317,40 @@ export default class RelatedCollection extends RecordArray { `Cannot replace a hasMany's state with a new state that contains duplicates.` ); - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; - mutateReplaceRelatedRecords(this, newValues, _TAG); + if (DEPRECATE_MANY_ARRAY_DUPLICATES) { + // 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, uniqueIdentifiers, _SIGNAL); + return result; + } + + // else, no dedupe, error on duplicates + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; + mutateReplaceRelatedRecords(this, newValues, _SIGNAL); return result; } - if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - // dedupe, no error + const newValues = extractIdentifiersFromRecords(adds); + assertNoDuplicates( + this, + target, + (currentState) => currentState.splice(start, deleteCount, ...newValues), + `Cannot splice a hasMany's state with a new state that contains duplicates.` + ); + + if (DEPRECATE_MANY_ARRAY_DUPLICATES) { + // dedupe const currentState = target.slice(); currentState.splice(start, deleteCount); const seen = new Set(currentState); - const unique: RecordInstance[] = []; + const unique: OpaqueRecordInstance[] = []; adds.forEach((item) => { const identifier = recordIdentifierFor(item); if (!seen.has(identifier)) { @@ -363,34 +360,26 @@ export default class RelatedCollection extends RecordArray { }); const newArgs = [start, deleteCount, ...unique]; - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; if (deleteCount > 0) { - mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _TAG); + mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _SIGNAL); } if (unique.length > 0) { - mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(unique), index: start }, _TAG); + mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(unique), index: start }, _SIGNAL); } return result; } // else, no dedupe, error on duplicates - const newValues = extractIdentifiersFromRecords(adds); - assertNoDuplicates( - this, - target, - (currentState) => currentState.splice(start, deleteCount, ...newValues), - `Cannot splice a hasMany's state with a new state that contains duplicates.` - ); - - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (deleteCount > 0) { - mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _TAG); + mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _SIGNAL); } if (newValues.length > 0) { - mutateAddToRelatedRecords(this, { value: newValues, index: start }, _TAG); + mutateAddToRelatedRecords(this, { value: newValues, index: start }, _SIGNAL); } return result; } @@ -400,19 +389,18 @@ export default class RelatedCollection extends RecordArray { } notify() { - const tag = this[IDENTIFIER_ARRAY_TAG]; - tag.shouldReset = true; - // @ts-expect-error + const signal = this[ARRAY_SIGNAL]; + signal.shouldReset = true; notifyArray(this); } /** Reloads all of the records in the manyArray. If the manyArray holds a relationship that was originally fetched using a links url - Ember Data will revisit the original links url to repopulate the + EmberData will revisit the original links url to repopulate the relationship. - If the manyArray holds the result of a `store.query()` reload will + If the ManyArray holds the result of a `store.query()` reload will re-run the original query. Example @@ -428,9 +416,9 @@ export default class RelatedCollection extends RecordArray { @method reload @public */ - reload(options?: FindOptions) { + reload(options?: BaseFinderOptions): Promise { // TODO this is odd, we don't ask the store for anything else like this? - return this._manager.reloadHasMany(this.key, options); + return this._manager.reloadHasMany(this.key, options) as Promise; } /** @@ -460,10 +448,10 @@ export default class RelatedCollection extends RecordArray { @param {Object} hash @return {Model} record */ - createRecord(hash: CreateRecordProperties): RecordInstance { + createRecord(hash: CreateRecordProperties): T { const { store } = this; assert(`Expected modelName to be set`, this.modelName); - const record = store.createRecord(this.modelName, hash); + const record = store.createRecord(this.modelName as TypeFromInstance, hash); this.push(record); return record; @@ -481,11 +469,11 @@ RelatedCollection.prototype._inverseIsAsync = false; RelatedCollection.prototype.key = ''; RelatedCollection.prototype.DEPRECATED_CLASS_NAME = 'ManyArray'; -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) { +function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxyRecord) { assert( - `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`, + `All elements of a hasMany relationship must be instances of Model, you passed ${typeof record}`, (function () { try { recordIdentifierFor(record); @@ -497,14 +485,14 @@ function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord ); } -function extractIdentifiersFromRecords(records: RecordInstance[]): StableRecordIdentifier[] { +function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableRecordIdentifier[] { return records.map(extractIdentifierFromRecord); } -function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance) { if (DEPRECATE_PROMISE_PROXIES) { if (isPromiseRecord(recordOrPromiseRecord)) { - let content = recordOrPromiseRecord.content; + 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 @@ -531,12 +519,12 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | return recordIdentifierFor(recordOrPromiseRecord); } -function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { - return !!record.then; +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); } -function assertNoDuplicates( - collection: RelatedCollection, +function assertNoDuplicates( + collection: RelatedCollection, target: StableRecordIdentifier[], callback: (currentState: StableRecordIdentifier[]) => void, reason: string @@ -547,23 +535,46 @@ function assertNoDuplicates( if (state.length !== new Set(state).size) { const duplicates = state.filter((currentValue, currentIndex) => state.indexOf(currentValue) !== currentIndex); - throw new Error( - `${reason} Found duplicates for the following records within the new state provided to \`<${ - collection.identifier.type - }:${collection.identifier.id || collection.identifier.lid}>.${collection.key}\`\n\t- ${Array.from( - new Set(duplicates) - ) - .map((r) => (isStableIdentifier(r) ? r.lid : recordIdentifierFor(r).lid)) - .sort((a, b) => a.localeCompare(b)) - .join('\n\t- ')}` - ); + if (DEPRECATE_MANY_ARRAY_DUPLICATES) { + deprecate( + `${reason} This behavior is deprecated. Found duplicates for the following records within the new state provided to \`<${ + collection.identifier.type + }:${collection.identifier.id || collection.identifier.lid}>.${collection.key}\`\n\t- ${Array.from( + new Set(duplicates) + ) + .map((r) => (isStableIdentifier(r) ? r.lid : recordIdentifierFor(r).lid)) + .sort((a, b) => a.localeCompare(b)) + .join('\n\t- ')}`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-many-array-duplicates', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.3', + available: '4.13', + }, + } + ); + } else { + throw new Error( + `${reason} Found duplicates for the following records within the new state provided to \`<${ + collection.identifier.type + }:${collection.identifier.id || collection.identifier.lid}>.${collection.key}\`\n\t- ${Array.from( + new Set(duplicates) + ) + .map((r) => (isStableIdentifier(r) ? r.lid : recordIdentifierFor(r).lid)) + .sort((a, b) => a.localeCompare(b)) + .join('\n\t- ')}` + ); + } } } -function mutateAddToRelatedRecords( - collection: RelatedCollection, +function mutateAddToRelatedRecords( + collection: RelatedCollection, operationInfo: { value: StableRecordIdentifier | StableRecordIdentifier[]; index?: number }, - _TAG: Tag + _SIGNAL: Signal ) { mutate( collection, @@ -573,14 +584,14 @@ function mutateAddToRelatedRecords( field: collection.key, ...operationInfo, }, - _TAG + _SIGNAL ); } -function mutateRemoveFromRelatedRecords( - collection: RelatedCollection, +function mutateRemoveFromRelatedRecords( + collection: RelatedCollection, operationInfo: { value: StableRecordIdentifier | StableRecordIdentifier[]; index?: number }, - _TAG: Tag + _SIGNAL: Signal ) { mutate( collection, @@ -590,18 +601,18 @@ function mutateRemoveFromRelatedRecords( field: collection.key, ...operationInfo, }, - _TAG + _SIGNAL ); } -function mutateReplaceRelatedRecord( - collection: RelatedCollection, +function mutateReplaceRelatedRecord( + collection: RelatedCollection, operationInfo: { value: StableRecordIdentifier; prior: StableRecordIdentifier; index: number; }, - _TAG: Tag + _SIGNAL: Signal ) { mutate( collection, @@ -611,11 +622,15 @@ function mutateReplaceRelatedRecord( field: collection.key, ...operationInfo, }, - _TAG + _SIGNAL ); } -function mutateReplaceRelatedRecords(collection: RelatedCollection, value: StableRecordIdentifier[], _TAG: Tag) { +function mutateReplaceRelatedRecords( + collection: RelatedCollection, + value: StableRecordIdentifier[], + _SIGNAL: Signal +) { mutate( collection, { @@ -624,11 +639,15 @@ function mutateReplaceRelatedRecords(collection: RelatedCollection, value: Stabl field: collection.key, value, }, - _TAG + _SIGNAL ); } -function mutateSortRelatedRecords(collection: RelatedCollection, value: StableRecordIdentifier[], _TAG: Tag) { +function mutateSortRelatedRecords( + collection: RelatedCollection, + value: StableRecordIdentifier[], + _SIGNAL: Signal +) { mutate( collection, { @@ -637,11 +656,15 @@ function mutateSortRelatedRecords(collection: RelatedCollection, value: StableRe field: collection.key, value, }, - _TAG + _SIGNAL ); } -function mutate(collection: RelatedCollection, mutation: Parameters[0], _TAG: Tag) { +function mutate( + collection: RelatedCollection, + mutation: Parameters[0], + _SIGNAL: Signal +) { collection._manager.mutate(mutation); - addToTransaction(_TAG); + addToTransaction(_SIGNAL); } diff --git a/packages/model/src/-private/model-for-mixin.ts b/packages/model/src/-private/model-for-mixin.ts index b21be165204..c17bd343887 100644 --- a/packages/model/src/-private/model-for-mixin.ts +++ b/packages/model/src/-private/model-for-mixin.ts @@ -2,14 +2,11 @@ import { getOwner } from '@ember/application'; import type Store from '@ember-data/store'; -import Model from './model'; +import { Model, type ModelFactory } from './model'; /* In case someone defined a relationship to a mixin, for example: - ``` - import Model, { belongsTo, hasMany } from '@ember-data/model'; - import Mixin from '@ember/object/mixin'; - + ```ts class CommentModel extends Model { @belongsTo('commentable', { polymorphic: true }) owner; } @@ -23,16 +20,16 @@ import Model from './model'; Model, so we can access the relationship CPs of the mixin (`comments`) in this case */ -export default function modelForMixin(store: Store, normalizedModelName: string): Model | null { - let owner: any = getOwner(store); - let MaybeMixin = owner.factoryFor(`mixin:${normalizedModelName}`); - let mixin = MaybeMixin && MaybeMixin.class; +export default function modelForMixin(store: Store, normalizedModelName: string): ModelFactory | undefined { + const owner = getOwner(store)!; + const MaybeMixin = owner.factoryFor(`mixin:${normalizedModelName}`); + const mixin = MaybeMixin && MaybeMixin.class; if (mixin) { - let ModelForMixin = Model.extend(mixin); + const ModelForMixin = Model.extend(mixin) as unknown as { __isMixin: boolean; __mixin: typeof mixin }; ModelForMixin.__isMixin = true; ModelForMixin.__mixin = mixin; //Cache the class as a model - owner.register('model:' + normalizedModelName, ModelForMixin); + owner.register(`model:${normalizedModelName}`, ModelForMixin); } - return owner.factoryFor(`model:${normalizedModelName}`); + return owner.factoryFor(`model:${normalizedModelName}`) as ModelFactory | undefined; } diff --git a/packages/model/src/-private/model-methods.ts b/packages/model/src/-private/model-methods.ts new file mode 100644 index 00000000000..ed8b690f087 --- /dev/null +++ b/packages/model/src/-private/model-methods.ts @@ -0,0 +1,160 @@ +import { importSync } from '@embroider/macros'; + +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +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'; +import type BelongsToReference from './references/belongs-to'; +import type HasManyReference from './references/has-many'; +import type { MaybeBelongsToFields, MaybeHasManyFields } from './type-utils'; + +export interface MinimalLegacyRecord { + errors: Errors; + ___recordState: RecordState; + currentState: RecordState; + isDestroyed: boolean; + isDestroying: boolean; + isReloading: boolean; + isValid: boolean; + [RecordStore]: Store; + + deleteRecord(): void; + unloadRecord(): void; + save(this: T, options?: Record): Promise; + destroyRecord(this: T, options?: Record): Promise; +} + +export function rollbackAttributes(this: T) { + const { currentState } = this; + const { isNew } = currentState; + + this[RecordStore]._join(() => { + peekCache(this).rollbackAttrs(recordIdentifierFor(this)); + this.errors.clear(); + currentState.cleanErrorRequests(); + if (isNew) { + this.unloadRecord(); + } + }); +} + +export function unloadRecord(this: T) { + if (this.currentState.isNew && (this.isDestroyed || this.isDestroying)) { + return; + } + this[RecordStore].unloadRecord(this); +} + +export function belongsTo>( + this: T, + prop: K +): BelongsToReference { + return lookupLegacySupport(this).referenceFor('belongsTo', prop) as BelongsToReference; +} + +export function hasMany>( + this: T, + prop: K +): HasManyReference { + return lookupLegacySupport(this).referenceFor('hasMany', prop) as HasManyReference; +} + +export function reload(this: T, options: Record = {}): Promise { + options.isReloading = true; + options.reload = true; + + const identifier = recordIdentifierFor(this); + assert(`You cannot reload a record without an ID`, identifier.id); + + this.isReloading = true; + const promise = this[RecordStore].request({ + op: 'findRecord', + data: { + options, + record: identifier, + }, + cacheOptions: { [Symbol.for('wd:skip-cache')]: true }, + }) + .then(() => this) + .finally(() => { + this.isReloading = false; + }); + + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + return deprecatedPromiseObject(promise); + } + + return promise; +} + +export function changedAttributes(this: T): ChangedAttributesHash { + return peekCache(this).changedAttrs(recordIdentifierFor(this)); +} + +export function serialize(this: T, options?: Record): unknown { + upgradeStore(this[RecordStore]); + return this[RecordStore].serializeRecord(this, options); +} + +export function deleteRecord(this: T): void { + // ensure we've populated currentState prior to deleting a new record + if (this.currentState) { + this[RecordStore].deleteRecord(this); + } +} + +export function save(this: T, options?: Record): Promise { + let promise: Promise; + + if (this.currentState.isNew && this.currentState.isDeleted) { + promise = Promise.resolve(this); + } else { + this.errors.clear(); + promise = this[RecordStore].saveRecord(this, options); + } + + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + return deprecatedPromiseObject(promise); + } + + return promise; +} + +export function destroyRecord(this: T, options?: Record): Promise { + const { isNew } = this.currentState; + this.deleteRecord(); + if (isNew) { + return Promise.resolve(this); + } + return this.save(options).then((_) => { + // run(() => { + this.unloadRecord(); + // }); + return this; + }); +} + +export function createSnapshot(this: T): Snapshot { + const store = this[RecordStore]; + + upgradeStore(store); + if (!store._fetchManager) { + const FetchManager = ( + importSync('@ember-data/legacy-compat/-private') as typeof import('@ember-data/legacy-compat/-private') + ).FetchManager; + store._fetchManager = new FetchManager(store); + } + + // @ts-expect-error Typescript isn't able to curry narrowed args that are divorced from each other. + return store._fetchManager.createSnapshot(recordIdentifierFor(this)); +} diff --git a/packages/model/src/-private/model.d.ts b/packages/model/src/-private/model.d.ts deleted file mode 100644 index c491dff024d..00000000000 --- a/packages/model/src/-private/model.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type EmberObject from '@ember/object'; - -import type { Errors } from '@ember-data/model/-private'; -import type Store from '@ember-data/store'; - -import type { AttributeSchema, RelationshipSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; -import type { JsonApiError } from '@ember-data/types/q/record-data-json-api'; -import type HasManyReference from './references/has-many'; -import type BelongsToReference from './references/belongs-to'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { LegacySupport } from './legacy-relationships-support'; -import type { Cache } from '@ember-data/types/q/cache'; -import type RecordState from './record-state'; - -export type ModelCreateArgs = { - _createProps: Record; - // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for - _secretInit: { - identifier: StableRecordIdentifier; - cache: Cache; - store: Store; - cb: (record: Model, cache: Cache, identifier: StableRecordIdentifier, store: Store) => void; - }; -}; - - -class Model extends EmberObject { - store: Store; - errors: Errors; - currentState: RecordState; - adapterError?: Error; - toString(): string; - save(): Promise; - hasMany(key: string): HasManyReference; - belongsTo(key: string): BelongsToReference - eachRelationship(callback: (this: T, key: string, meta: RelationshipSchema) => void, binding?: T): void; - eachAttribute(callback: (this: T, key: string, meta: AttributeSchema) => void, binding?: T): void; - invalidErrorsChanged(errors: JsonApiError[]): void; - rollbackAttributes(): void; - changedAttributes(): Record; - [key: string]: unknown; - isSaving: boolean; - isNew: boolean; - isDeleted: boolean; - hasDirtyAttributes: boolean; - deleteRecord(): void; - unloadRecord(): void; - serialize(): Record; - - static modelName: string; - static fields: Map; - static attributes: Map; - static relationshipsByName: Map; - static eachAttribute(callback: (this: T, key: string, attribute: AttributeSchema) => void, binding?: T): void; - static eachRelationship(callback: (this: T, key: string, relationship: RelationshipSchema) => void, binding?: T): void; - static eachTransformedAttribute(callback: (this: T, key: string, type: string | null) => void, binding?: T): void; - - static toString(): string; - static isModel: true; - static relationshipsObject: RelationshipsSchema; - static extend(...mixins: unknown[]): typeof Model; - static reopenClass(...mixins: unknown[]): void; - static create(createArgs: ModelCreateArgs): Model; - static __isMixin?: true; - static __mixin?: unknown; -} - -interface Model { - constructor: typeof Model; -} - -export default Model; - -export type StaticModel = typeof Model; - -export const LEGACY_SUPPORT: Map; - -export type ModelFactory = { class: StaticModel }; -export type FactoryCache = Record; -// we put this on the store for interop because it's used by modelFor and -// instantiateRecord as well. -export type ModelStore = Store & { _modelFactoryCache: FactoryCache }; diff --git a/packages/model/src/-private/model.js b/packages/model/src/-private/model.js deleted file mode 100644 index 4cd3f263206..00000000000 --- a/packages/model/src/-private/model.js +++ /dev/null @@ -1,2540 +0,0 @@ -/** - @module @ember-data/model - */ - -import { assert, deprecate, warn } from '@ember/debug'; -import EmberObject from '@ember/object'; -import { dependentKeyCompat } from '@ember/object/compat'; -import { run } from '@ember/runloop'; -import { tracked } from '@glimmer/tracking'; -import Ember from 'ember'; - -import { importSync } from '@embroider/macros'; - -import { - DEPRECATE_EARLY_STATIC, - DEPRECATE_MODEL_REOPEN, - DEPRECATE_NON_EXPLICIT_POLYMORPHISM, - DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, - DEPRECATE_SAVE_PROMISE_ACCESS, -} from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import { HAS_DEBUG_PACKAGE } from '@ember-data/packages'; -import { recordIdentifierFor, storeFor } from '@ember-data/store'; -import { coerceId, peekCache } from '@ember-data/store/-private'; - -import { deprecatedPromiseObject } from './deprecated-promise-proxy'; -import Errors from './errors'; -import { LegacySupport } from './legacy-relationships-support'; -import notifyChanges from './notify-changes'; -import RecordState, { peekTag, tagged } from './record-state'; -import { relationshipFromMeta } from './relationship-meta'; - -const { changeProperties } = Ember; -export const LEGACY_SUPPORT = new Map(); - -export function lookupLegacySupport(record) { - const identifier = recordIdentifierFor(record); - let support = LEGACY_SUPPORT.get(identifier); - - if (!support) { - assert(`Memory Leak Detected`, !record.isDestroyed && !record.isDestroying); - support = new LegacySupport(record); - LEGACY_SUPPORT.set(identifier, support); - LEGACY_SUPPORT.set(record, support); - } - - return support; -} - -function findPossibleInverses(type, inverseType, name, relationshipsSoFar) { - let possibleRelationships = relationshipsSoFar || []; - - let relationshipMap = inverseType.relationships; - if (!relationshipMap) { - return possibleRelationships; - } - - let relationshipsForType = relationshipMap.get(type.modelName); - let relationships = Array.isArray(relationshipsForType) - ? relationshipsForType.filter((relationship) => { - let optionsForRelationship = relationship.options; - - if (!optionsForRelationship.inverse && optionsForRelationship.inverse !== null) { - return true; - } - - return name === optionsForRelationship.inverse; - }) - : null; - - if (relationships) { - possibleRelationships.push.apply(possibleRelationships, relationships); - } - - //Recurse to support polymorphism - if (type.superclass) { - findPossibleInverses(type.superclass, inverseType, name, possibleRelationships); - } - - return possibleRelationships; -} - -/* - * This decorator allows us to lazily compute - * an expensive getter on first-access and thereafter - * never recompute it. - */ -function computeOnce(target, key, desc) { - const cache = new WeakMap(); - let getter = desc.get; - desc.get = function () { - let meta = cache.get(this); - - if (!meta) { - meta = { hasComputed: false, value: undefined }; - cache.set(this, meta); - } - - if (!meta.hasComputed) { - meta.value = getter.call(this); - meta.hasComputed = true; - } - - return meta.value; - }; - return desc; -} - -/** - Base class from which Models can be defined. - - ```js - import Model, { attr } from '@ember-data/model'; - - export default class User extends Model { - @attr name; - } - ``` - - @class Model - @public - @extends Ember.EmberObject -*/ -class Model extends EmberObject { - ___private_notifications; - - init(options = {}) { - if (DEBUG) { - if (!options._secretInit && !options._createProps) { - throw new Error( - 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' - ); - } - } - const createProps = options._createProps; - const _secretInit = options._secretInit; - options._createProps = null; - options._secretInit = null; - - let store = (this.store = _secretInit.store); - super.init(options); - - let identity = _secretInit.identifier; - _secretInit.cb(this, _secretInit.cache, identity, _secretInit.store); - - this.___recordState = DEBUG ? new RecordState(this) : null; - - this.setProperties(createProps); - - let notifications = store.notifications; - this.___private_notifications = notifications.subscribe(identity, (identifier, type, key) => { - notifyChanges(identifier, type, key, this, store); - }); - } - - destroy() { - const identifier = recordIdentifierFor(this); - this.___recordState?.destroy(); - const store = storeFor(this); - store.notifications.unsubscribe(this.___private_notifications); - // Legacy behavior is to notify the relationships on destroy - // such that they "clear". It's uncertain this behavior would - // be good for a new model paradigm, likely cheaper and safer - // to simply not notify, for this reason the store does not itself - // notify individual changes once the delete has been signaled, - // this decision is left to model instances. - - this.eachRelationship((key, meta) => { - if (meta.kind === 'belongsTo') { - this.notifyPropertyChange(key); - } - }); - LEGACY_SUPPORT.get(this)?.destroy(); - LEGACY_SUPPORT.delete(this); - LEGACY_SUPPORT.delete(identifier); - - super.destroy(); - } - - /** - If this property is `true` the record is in the `empty` - state. Empty is the first state all records enter after they have - been created. Most records created by the store will quickly - transition to the `loading` state if data needs to be fetched from - the server or the `created` state if the record is created on the - client. A record can also enter the empty state if the adapter is - unable to locate the record. - - @property isEmpty - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isEmpty() { - return this.currentState.isEmpty; - } - - /** - If this property is `true` the record is in the `loading` state. A - record enters this state when the store asks the adapter for its - data. It remains in this state until the adapter provides the - requested data. - - @property isLoading - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isLoading() { - return this.currentState.isLoading; - } - - /** - If this property is `true` the record is in the `loaded` state. A - record enters this state when its data is populated. Most of a - record's lifecycle is spent inside substates of the `loaded` - state. - - Example - - ```javascript - let record = store.createRecord('model'); - record.isLoaded; // true - - store.findRecord('model', 1).then(function(model) { - model.isLoaded; // true - }); - ``` - - @property isLoaded - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isLoaded() { - return this.currentState.isLoaded; - } - - /** - If this property is `true` the record is in the `dirty` state. The - record has local changes that have not yet been saved by the - adapter. This includes records that have been created (but not yet - saved) or deleted. - - Example - - ```javascript - let record = store.createRecord('model'); - record.hasDirtyAttributes; // true - - store.findRecord('model', 1).then(function(model) { - model.hasDirtyAttributes; // false - model.set('foo', 'some value'); - model.hasDirtyAttributes; // true - }); - ``` - - @since 1.13.0 - @property hasDirtyAttributes - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get hasDirtyAttributes() { - return this.currentState.isDirty; - } - - /** - If this property is `true` the record is in the `saving` state. A - record enters the saving state when `save` is called, but the - adapter has not yet acknowledged that the changes have been - persisted to the backend. - - Example - - ```javascript - let record = store.createRecord('model'); - record.isSaving; // false - let promise = record.save(); - record.isSaving; // true - promise.then(function() { - record.isSaving; // false - }); - ``` - - @property isSaving - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isSaving() { - return this.currentState.isSaving; - } - - /** - If this property is `true` the record is in the `deleted` state - and has been marked for deletion. When `isDeleted` is true and - `hasDirtyAttributes` is true, the record is deleted locally but the deletion - was not yet persisted. When `isSaving` is true, the change is - in-flight. When both `hasDirtyAttributes` and `isSaving` are false, the - change has persisted. - - Example - - ```javascript - let record = store.createRecord('model'); - record.isDeleted; // false - record.deleteRecord(); - - // Locally deleted - record.isDeleted; // true - record.hasDirtyAttributes; // true - record.isSaving; // false - - // Persisting the deletion - let promise = record.save(); - record.isDeleted; // true - record.isSaving; // true - - // Deletion Persisted - promise.then(function() { - record.isDeleted; // true - record.isSaving; // false - record.hasDirtyAttributes; // false - }); - ``` - - @property isDeleted - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isDeleted() { - return this.currentState.isDeleted; - } - - /** - If this property is `true` the record is in the `new` state. A - record will be in the `new` state when it has been created on the - client and the adapter has not yet report that it was successfully - saved. - - Example - - ```javascript - let record = store.createRecord('model'); - record.isNew; // true - - record.save().then(function(model) { - model.isNew; // false - }); - ``` - - @property isNew - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isNew() { - return this.currentState.isNew; - } - - /** - If this property is `true` the record is in the `valid` state. - - A record will be in the `valid` state when the adapter did not report any - server-side validation failures. - - @property isValid - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isValid() { - return this.currentState.isValid; - } - - /** - If the record is in the dirty state this property will report what - kind of change has caused it to move into the dirty - state. Possible values are: - - - `created` The record has been created by the client and not yet saved to the adapter. - - `updated` The record has been updated by the client and not yet saved to the adapter. - - `deleted` The record has been deleted by the client and not yet saved to the adapter. - - Example - - ```javascript - let record = store.createRecord('model'); - record.dirtyType; // 'created' - ``` - - @property dirtyType - @public - @type {String} - @readOnly - */ - @dependentKeyCompat - get dirtyType() { - return this.currentState.dirtyType; - } - - /** - If `true` the adapter reported that it was unable to save local - changes to the backend for any reason other than a server-side - validation error. - - Example - - ```javascript - record.isError; // false - record.set('foo', 'valid value'); - record.save().then(null, function() { - record.isError; // true - }); - ``` - - @property isError - @public - @type {Boolean} - @readOnly - */ - @dependentKeyCompat - get isError() { - return this.currentState.isError; - } - set isError(v) { - if (DEBUG) { - throw new Error(`isError is not directly settable`); - } - } - - /** - If `true` the store is attempting to reload the record from the adapter. - - Example - - ```javascript - record.isReloading; // false - record.reload(); - record.isReloading; // true - ``` - - @property isReloading - @public - @type {Boolean} - @readOnly - */ - @tracked isReloading = false; - - /** - All ember models have an id property. This is an identifier - managed by an external source. These are always coerced to be - strings before being used internally. Note when declaring the - attributes for a model it is an error to declare an id - attribute. - - ```javascript - let record = store.createRecord('model'); - record.id; // null - - store.findRecord('model', 1).then(function(model) { - model.id; // '1' - }); - ``` - - @property id - @public - @type {String} - */ - @tagged - get id() { - // this guard exists, because some dev-only deprecation code - // (addListener via validatePropertyInjections) invokes toString before the - // object is real. - if (DEBUG) { - try { - return recordIdentifierFor(this).id; - } catch { - return void 0; - } - } - return recordIdentifierFor(this).id; - } - set id(id) { - const normalizedId = coerceId(id); - const identifier = recordIdentifierFor(this); - let didChange = normalizedId !== identifier.id; - assert( - `Cannot set ${identifier.type} record's id to ${id}, because id is already ${identifier.id}`, - !didChange || identifier.id === null - ); - - if (normalizedId !== null && didChange) { - this.store._instanceCache.setRecordId(identifier, normalizedId); - this.store.notifications.notify(identifier, 'identity'); - } - } - - toString() { - return ``; - } - - /** - @property currentState - @private - @type {Object} - */ - // TODO we can probably make this a computeOnce - // we likely do not need to notify the currentState root anymore - @tagged - get currentState() { - // descriptors are called with the wrong `this` context during mergeMixins - // when using legacy/classic ember classes. Basically: lazy in prod and eager in dev. - // so we do this to try to steer folks to the nicer "dont user currentState" - // error. - if (!DEBUG) { - if (!this.___recordState) { - this.___recordState = new RecordState(this); - } - } - return this.___recordState; - } - set currentState(_v) { - throw new Error('cannot set currentState'); - } - - /** - The store service instance which created this record instance - - @property store - @public - */ - - /** - When the record is in the `invalid` state this object will contain - any errors returned by the adapter. When present the errors hash - contains keys corresponding to the invalid property names - and values which are arrays of Javascript objects with two keys: - - - `message` A string containing the error message from the backend - - `attribute` The name of the property associated with this error message - - ```javascript - record.errors.length; // 0 - record.set('foo', 'invalid value'); - record.save().catch(function() { - record.errors.foo; - // [{message: 'foo should be a number.', attribute: 'foo'}] - }); - ``` - - The `errors` property is useful for displaying error messages to - the user. - - ```handlebars - - {{#each @model.errors.username as |error|}} -
- {{error.message}} -
- {{/each}} - - {{#each @model.errors.email as |error|}} -
- {{error.message}} -
- {{/each}} - ``` - - - You can also access the special `messages` property on the error - object to get an array of all the error strings. - - ```handlebars - {{#each @model.errors.messages as |message|}} -
- {{message}} -
- {{/each}} - ``` - - @property errors - @public - @type {Errors} - */ - @computeOnce - get errors() { - let errors = Errors.create({ __record: this }); - this.currentState.updateInvalidErrors(errors); - return errors; - } - - /** - This property holds the `AdapterError` object with which - last adapter operation was rejected. - - @property adapterError - @public - @type {AdapterError} - */ - @dependentKeyCompat - get adapterError() { - return this.currentState.adapterError; - } - set adapterError(v) { - throw new Error(`adapterError is not directly settable`); - } - - /** - Create a JSON representation of the record, using the serialization - strategy of the store's adapter. - - `serialize` takes an optional hash as a parameter, currently - supported options are: - - - `includeId`: `true` if the record's ID should be included in the - JSON representation. - - @method serialize - @public - @param {Object} options - @return {Object} an object whose values are primitive JSON values only - */ - serialize(options) { - return storeFor(this).serializeRecord(this, options); - } - - /* - We hook the default implementation to ensure - our tagged properties are properly notified - as well. We still super for everything because - sync observers require a direct call occuring - to trigger their flush. We wouldn't need to - super in 4.0+ where sync observers are removed. - */ - notifyPropertyChange(key) { - let tag = peekTag(this, key); - if (tag) { - tag.notify(); - } - super.notifyPropertyChange(key); - } - - /** - Marks the record as deleted but does not save it. You must call - `save` afterwards if you want to persist it. You might use this - method if you want to allow the user to still `rollbackAttributes()` - after a delete was made. - - Example - - ```app/controllers/model/delete.js - import Controller from '@ember/controller'; - import { action } from '@ember/object'; - - export default class ModelDeleteController extends Controller { - @action - softDelete() { - this.model.deleteRecord(); - } - - @action - confirm() { - this.model.save(); - } - - @action - undo() { - this.model.rollbackAttributes(); - } - } - ``` - - @method deleteRecord - @public - */ - deleteRecord() { - // ensure we've populated currentState prior to deleting a new record - if (this.currentState) { - storeFor(this).deleteRecord(this); - } - } - - /** - Same as `deleteRecord`, but saves the record immediately. - - Example - - ```app/controllers/model/delete.js - import Controller from '@ember/controller'; - import { action } from '@ember/object'; - - export default class ModelDeleteController extends Controller { - @action - delete() { - this.model.destroyRecord().then(function() { - this.transitionToRoute('model.index'); - }); - } - } - ``` - - If you pass an object on the `adapterOptions` property of the options - argument it will be passed to your adapter via the snapshot - - ```js - record.destroyRecord({ adapterOptions: { subscribe: false } }); - ``` - - ```app/adapters/post.js - import MyCustomAdapter from './custom-adapter'; - - export default class PostAdapter extends MyCustomAdapter { - deleteRecord(store, type, snapshot) { - if (snapshot.adapterOptions.subscribe) { - // ... - } - // ... - } - } - ``` - - @method destroyRecord - @public - @param {Object} options - @return {Promise} a promise that will be resolved when the adapter returns - successfully or rejected if the adapter returns with an error. - */ - destroyRecord(options) { - const { isNew } = this.currentState; - this.deleteRecord(); - if (isNew) { - return Promise.resolve(this); - } - return this.save(options).then((_) => { - run(() => { - this.unloadRecord(); - }); - return this; - }); - } - - /** - Unloads the record from the store. This will not send a delete request - to your server, it just unloads the record from memory. - - @method unloadRecord - @public - */ - unloadRecord() { - if (this.currentState.isNew && (this.isDestroyed || this.isDestroying)) { - return; - } - storeFor(this).unloadRecord(this); - } - - /** - @method _notifyProperties - @private - */ - _notifyProperties(keys) { - // changeProperties defers notifications until after the delegate - // and protects with a try...finally block - // previously used begin...endPropertyChanges but this is private API - changeProperties(() => { - let key; - for (let i = 0, length = keys.length; i < length; i++) { - key = keys[i]; - this.notifyPropertyChange(key); - } - }); - } - - /** - Returns an object, whose keys are changed properties, and value is - an [oldProp, newProp] array. - - The array represents the diff of the canonical state with the local state - of the model. Note: if the model is created locally, the canonical state is - empty since the adapter hasn't acknowledged the attributes yet: - - Example - - ```app/models/mascot.js - import Model, { attr } from '@ember-data/model'; - - export default class MascotModel extends Model { - @attr('string') name; - @attr('boolean', { - defaultValue: false - }) - isAdmin; - } - ``` - - ```javascript - let mascot = store.createRecord('mascot'); - - mascot.changedAttributes(); // {} - - mascot.set('name', 'Tomster'); - mascot.changedAttributes(); // { name: [undefined, 'Tomster'] } - - mascot.set('isAdmin', true); - mascot.changedAttributes(); // { isAdmin: [undefined, true], name: [undefined, 'Tomster'] } - - mascot.save().then(function() { - mascot.changedAttributes(); // {} - - mascot.set('isAdmin', false); - mascot.changedAttributes(); // { isAdmin: [true, false] } - }); - ``` - - @method changedAttributes - @public - @return {Object} an object, whose keys are changed properties, - and value is an [oldProp, newProp] array. - */ - changedAttributes() { - return peekCache(this).changedAttrs(recordIdentifierFor(this)); - } - - /** - If the model `hasDirtyAttributes` this function will discard any unsaved - changes. If the model `isNew` it will be removed from the store. - - Example - - ```javascript - record.name; // 'Untitled Document' - record.set('name', 'Doc 1'); - record.name; // 'Doc 1' - record.rollbackAttributes(); - record.name; // 'Untitled Document' - ``` - - @since 1.13.0 - @method rollbackAttributes - @public - */ - rollbackAttributes() { - const { currentState } = this; - const { isNew } = currentState; - - storeFor(this)._join(() => { - peekCache(this).rollbackAttrs(recordIdentifierFor(this)); - this.errors.clear(); - currentState.cleanErrorRequests(); - if (isNew) { - this.unloadRecord(); - } - }); - } - - /** - @method _createSnapshot - @private - */ - // TODO @deprecate in favor of a public API or examples of how to test successfully - _createSnapshot() { - const store = storeFor(this); - - if (!store._fetchManager) { - const FetchManager = importSync('@ember-data/legacy-compat/-private').FetchManager; - store._fetchManager = new FetchManager(store); - } - - return store._fetchManager.createSnapshot(recordIdentifierFor(this)); - } - - /** - Save the record and persist any changes to the record to an - external source via the adapter. - - Example - - ```javascript - record.set('name', 'Tomster'); - record.save().then(function() { - // Success callback - }, function() { - // Error callback - }); - ``` - - If you pass an object using the `adapterOptions` property of the options - argument it will be passed to your adapter via the snapshot. - - ```js - record.save({ adapterOptions: { subscribe: false } }); - ``` - - ```app/adapters/post.js - import MyCustomAdapter from './custom-adapter'; - - export default class PostAdapter extends MyCustomAdapter { - updateRecord(store, type, snapshot) { - if (snapshot.adapterOptions.subscribe) { - // ... - } - // ... - } - } - ``` - - @method save - @public - @param {Object} options - @return {Promise} a promise that will be resolved when the adapter returns - successfully or rejected if the adapter returns with an error. - */ - save(options) { - let promise; - - if (this.currentState.isNew && this.currentState.isDeleted) { - promise = Promise.resolve(this); - } else { - promise = storeFor(this).saveRecord(this, options); - } - - if (DEPRECATE_SAVE_PROMISE_ACCESS) { - return deprecatedPromiseObject(promise); - } - - return promise; - } - - /** - Reload the record from the adapter. - - This will only work if the record has already finished loading. - - Example - - ```app/controllers/model/view.js - import Controller from '@ember/controller'; - import { action } from '@ember/object'; - - export default class ViewController extends Controller { - @action - reload() { - this.model.reload().then(function(model) { - // do something with the reloaded model - }); - } - } - ``` - - @method reload - @public - @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter request - - @return {Promise} a promise that will be resolved with the record when the - adapter returns successfully or rejected if the adapter returns - with an error. - */ - reload(options = {}) { - options.isReloading = true; - options.reload = true; - - const identifier = recordIdentifierFor(this); - assert(`You cannot reload a record without an ID`, identifier.id); - - this.isReloading = true; - const promise = storeFor(this) - .request({ - op: 'findRecord', - data: { - options, - record: identifier, - }, - cacheOptions: { [Symbol.for('ember-data:skip-cache')]: true }, - }) - .then(() => this) - .finally(() => { - this.isReloading = false; - }); - - if (DEPRECATE_SAVE_PROMISE_ACCESS) { - return deprecatedPromiseObject(promise); - } - return promise; - } - - attr() { - assert( - 'The `attr` method is not available on Model, a Snapshot was probably expected. Are you passing a Model instead of a Snapshot to your serializer?', - false - ); - } - - /** - Get the reference for the specified belongsTo relationship. - - Example - - ```app/models/blog.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class BlogModel extends Model { - @belongsTo('user', { async: true, inverse: null }) user; - } - ``` - - ```javascript - let blog = store.push({ - data: { - type: 'blog', - id: 1, - relationships: { - user: { - data: { type: 'user', id: 1 } - } - } - } - }); - let userRef = blog.belongsTo('user'); - - // check if the user relationship is loaded - let isLoaded = userRef.value() !== null; - - // get the record of the reference (null if not yet available) - let user = userRef.value(); - - // get the identifier of the reference - if (userRef.remoteType() === "id") { - let id = userRef.id(); - } else if (userRef.remoteType() === "link") { - let link = userRef.link(); - } - - // load user (via store.findRecord or store.findBelongsTo) - userRef.load().then(...) - - // or trigger a reload - userRef.reload().then(...) - - // provide data for reference - userRef.push({ - type: 'user', - id: 1, - attributes: { - username: "@user" - } - }).then(function(user) { - userRef.value() === user; - }); - ``` - - @method belongsTo - @public - @param {String} name of the relationship - @since 2.5.0 - @return {BelongsToReference} reference for this relationship - */ - belongsTo(name) { - return lookupLegacySupport(this).referenceFor('belongsTo', name); - } - - /** - Get the reference for the specified hasMany relationship. - - Example - - ```app/models/blog.js - import Model, { hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('comment', { async: true, inverse: null }) comments; - } - - let blog = store.push({ - data: { - type: 'blog', - id: 1, - relationships: { - comments: { - data: [ - { type: 'comment', id: 1 }, - { type: 'comment', id: 2 } - ] - } - } - } - }); - let commentsRef = blog.hasMany('comments'); - - // check if the comments are loaded already - let isLoaded = commentsRef.value() !== null; - - // get the records of the reference (null if not yet available) - let comments = commentsRef.value(); - - // get the identifier of the reference - if (commentsRef.remoteType() === "ids") { - let ids = commentsRef.ids(); - } else if (commentsRef.remoteType() === "link") { - let link = commentsRef.link(); - } - - // load comments (via store.findMany or store.findHasMany) - commentsRef.load().then(...) - - // or trigger a reload - commentsRef.reload().then(...) - - // provide data for reference - commentsRef.push([{ type: 'comment', id: 1 }, { type: 'comment', id: 2 }]).then(function(comments) { - commentsRef.value() === comments; - }); - ``` - - @method hasMany - @public - @param {String} name of the relationship - @since 2.5.0 - @return {HasManyReference} reference for this relationship - */ - hasMany(name) { - return lookupLegacySupport(this).referenceFor('hasMany', name); - } - - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - - The callback method you provide should have the following signature (all - parameters are optional): - - ```javascript - function(name, descriptor); - ``` - - - `name` the name of the current property in the iteration - - `descriptor` the meta object that describes this relationship - - The relationship descriptor argument is an object with the following properties. - - - **key** String the name of this relationship on the Model - - **kind** String "hasMany" or "belongsTo" - - **options** Object the original options hash passed when the relationship was declared - - **parentType** Model the type of the Model that owns this relationship - - **type** String the type name of the related Model - - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. - - Example - - ```app/serializers/application.js - import JSONSerializer from '@ember-data/serializer/json'; - - export default class ApplicationSerializer extends JSONSerializer { - serialize(record, options) { - let json = {}; - - record.eachRelationship(function(name, descriptor) { - if (descriptor.kind === 'hasMany') { - let serializedHasManyName = name.toUpperCase() + '_IDS'; - json[serializedHasManyName] = record.get(name).map(r => r.id); - } - }); - - return json; - } - } - ``` - - @method eachRelationship - @public - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - eachRelationship(callback, binding) { - this.constructor.eachRelationship(callback, binding); - } - - relationshipFor(name) { - return this.constructor.relationshipsByName.get(name); - } - - inverseFor(key) { - return this.constructor.inverseFor(key, storeFor(this)); - } - - eachAttribute(callback, binding) { - this.constructor.eachAttribute(callback, binding); - } - - static isModel = true; - - /** - Create should only ever be called by the store. To create an instance of a - `Model` in a dirty state use `store.createRecord`. - - To create instances of `Model` in a clean state, use `store.push` - - @method create - @private - @static - */ - - /** - Represents the model's class name as a string. This can be used to look up the model's class name through - `Store`'s modelFor method. - - `modelName` is generated for you by Ember Data. It will be a lowercased, dasherized string. - For example: - - ```javascript - store.modelFor('post').modelName; // 'post' - store.modelFor('blog-post').modelName; // 'blog-post' - ``` - - The most common place you'll want to access `modelName` is in your serializer's `payloadKeyFromModelName` method. For example, to change payload - keys to underscore (instead of dasherized), you might use the following code: - - ```javascript - import RESTSerializer from '@ember-data/serializer/rest'; - import { underscore } from '/utils/string-utils'; - - export default const PostSerializer = RESTSerializer.extend({ - payloadKeyFromModelName(modelName) { - return underscore(modelName); - } - }); - ``` - @property modelName - @public - @type String - @readonly - @static - */ - static modelName = null; - - /* - These class methods below provide relationship - introspection abilities about relationships. - - A note about the computed properties contained here: - - **These properties are effectively sealed once called for the first time.** - To avoid repeatedly doing expensive iteration over a model's fields, these - values are computed once and then cached for the remainder of the runtime of - your application. - - If your application needs to modify a class after its initial definition - (for example, using `reopen()` to add additional attributes), make sure you - do it before using your model with the store, which uses these properties - extensively. - */ - - /** - For a given relationship name, returns the model type of the relationship. - - For example, if you define a model like this: - - ```app/models/post.js - import Model, { hasMany } from '@ember-data/model'; - - export default class PostModel extends Model { - @hasMany('comment') comments; - } - ``` - - Calling `store.modelFor('post').typeForRelationship('comments', store)` will return `Comment`. - - @method typeForRelationship - @public - @static - @param {String} name the name of the relationship - @param {store} store an instance of Store - @return {Model} the type of the relationship, or undefined - */ - static typeForRelationship(name, store) { - 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.`, - 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 - ); - } - let relationship = this.relationshipsByName.get(name); - return relationship && store.modelFor(relationship.type); - } - - @computeOnce - static get inverseMap() { - 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.`, - 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); - } - - /** - Find the relationship which is the inverse of the one asked for. - - For example, if you define models like this: - - ```app/models/post.js - import Model, { hasMany } from '@ember-data/model'; - - export default class PostModel extends Model { - @hasMany('message') comments; - } - ``` - - ```app/models/message.js - import Model, { belongsTo } from '@ember-data/model'; - - export default class MessageModel extends Model { - @belongsTo('post') owner; - } - ``` - - ``` js - store.modelFor('post').inverseFor('comments', store) // { type: App.Message, name: 'owner', kind: 'belongsTo' } - store.modelFor('message').inverseFor('owner', store) // { type: App.Post, name: 'comments', kind: 'hasMany' } - ``` - - @method inverseFor - @public - @static - @param {String} name the name of the relationship - @param {Store} store - @return {Object} the inverse relationship, or null - */ - static inverseFor(name, store) { - 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.`, - 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 - ); - } - let inverseMap = this.inverseMap; - if (inverseMap[name]) { - return inverseMap[name]; - } else { - let inverse = this._findInverseFor(name, store); - inverseMap[name] = inverse; - return inverse; - } - } - - //Calculate the inverse, ignoring the cache - static _findInverseFor(name, store) { - 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.`, - 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); - 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.getSchemaDefinitionService().doesTypeExist(relationship.type); - - if (isExplicitInverseNull || isAbstractType) { - assert( - `No schema for the abstract type '${relationship.type}' for the polymorphic relationship '${name}' on '${this.modelName}' was provided by the SchemaDefinitionService.`, - !isPolymorphic || isExplicitInverseNull - ); - return null; - } - - let fieldOnInverse, inverseKind, inverseRelationship, inverseOptions; - let inverseSchema = this.typeForRelationship(name, store); - - // 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 && 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 '${this.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 - if (relationship.type === relationship.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(this, inverseSchema, name); - - if (possibleRelationships.length === 0) { - return null; - } - - if (DEBUG) { - let filteredRelationships = possibleRelationships.filter((possibleRelationship) => { - let optionsForRelationship = possibleRelationship.options; - return name === optionsForRelationship.inverse; - }); - - assert( - "You defined the '" + - name + - "' relationship on " + - this + - ', 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 - ); - } - - let explicitRelationship = possibleRelationships.find((relationship) => relationship.options.inverse === name); - if (explicitRelationship) { - possibleRelationships = [explicitRelationship]; - } - - assert( - "You defined the '" + - name + - "' relationship on " + - this + - ', but multiple possible inverse relationships of type ' + - this + - ' were found on ' + - 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; - } - - // 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 '${this.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 '${this.modelName}' is misconfigured.`, - options.as - ); - assert( - `options.as should match the expected type of the polymorphic relationship. Expected field '${name}' on type '${this.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 ${this.modelName}:${name}.`, - inverseOptions.inverse !== null - ); - - return { - type: inverseSchema, - name: fieldOnInverse, - kind: inverseKind, - options: inverseOptions, - }; - } - - /** - The model's relationships as a map, keyed on the type of the - relationship. The value of each entry is an array containing a descriptor - for each relationship with that type, describing the name of the relationship - as well as the type. - - For example, given the following model definition: - - ```app/models/blog.js - import Model, { belongsTo, hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('user') users; - @belongsTo('user') owner; - @hasMany('post') posts; - } - ``` - - This computed property would return a map describing these - relationships, like this: - - ```javascript - import { get } from '@ember/object'; - import Blog from 'app/models/blog'; - import User from 'app/models/user'; - import Post from 'app/models/post'; - - let relationships = Blog.relationships; - relationships.user; - //=> [ { name: 'users', kind: 'hasMany' }, - // { name: 'owner', kind: 'belongsTo' } ] - relationships.post; - //=> [ { name: 'posts', kind: 'hasMany' } ] - ``` - - @property relationships - @public - @static - @type Map - @readOnly - */ - - @computeOnce - static get relationships() { - 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.`, - 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 - ); - } - let map = new Map(); - let relationshipsByName = this.relationshipsByName; - - // Loop through each computed property on the class - relationshipsByName.forEach((desc) => { - let { type } = desc; - - if (!map.has(type)) { - map.set(type, []); - } - - map.get(type).push(desc); - }); - - return map; - } - - /** - A hash containing lists of the model's relationships, grouped - by the relationship kind. For example, given a model with this - definition: - - ```app/models/blog.js - import Model, { belongsTo, hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('user') users; - @belongsTo('user') owner; - - @hasMany('post') posts; - } - ``` - - This property would contain the following: - - ```javascript - import { get } from '@ember/object'; - import Blog from 'app/models/blog'; - - let relationshipNames = Blog.relationshipNames; - relationshipNames.hasMany; - //=> ['users', 'posts'] - relationshipNames.belongsTo; - //=> ['owner'] - ``` - - @property relationshipNames - @public - @static - @type Object - @readOnly - */ - @computeOnce - static get relationshipNames() { - 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.`, - 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 - ); - } - let names = { - hasMany: [], - belongsTo: [], - }; - - this.eachComputedProperty((name, meta) => { - if (meta.isRelationship) { - names[meta.kind].push(name); - } - }); - - return names; - } - - /** - An array of types directly related to a model. Each type will be - included once, regardless of the number of relationships it has with - the model. - - For example, given a model with this definition: - - ```app/models/blog.js - import Model, { belongsTo, hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('user') users; - @belongsTo('user') owner; - - @hasMany('post') posts; - } - ``` - - This property would contain the following: - - ```javascript - import { get } from '@ember/object'; - import Blog from 'app/models/blog'; - - let relatedTypes = Blog.relatedTypes'); - //=> ['user', 'post'] - ``` - - @property relatedTypes - @public - @static - @type Ember.Array - @readOnly - */ - @computeOnce - static get relatedTypes() { - 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.`, - 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 - ); - } - let types = []; - - let rels = this.relationshipsObject; - let relationships = Object.keys(rels); - - // create an array of the unique types involved - // in relationships - for (let i = 0; i < relationships.length; i++) { - let name = relationships[i]; - let meta = rels[name]; - let modelName = meta.type; - - if (types.indexOf(modelName) === -1) { - types.push(modelName); - } - } - - return types; - } - - /** - A map whose keys are the relationships of a model and whose values are - relationship descriptors. - - For example, given a model with this - definition: - - ```app/models/blog.js - import Model, { belongsTo, hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('user') users; - @belongsTo('user') owner; - - @hasMany('post') posts; - } - ``` - - This property would contain the following: - - ```javascript - import { get } from '@ember/object'; - import Blog from 'app/models/blog'; - - let relationshipsByName = Blog.relationshipsByName; - relationshipsByName.users; - //=> { key: 'users', kind: 'hasMany', type: 'user', options: Object, isRelationship: true } - relationshipsByName.owner; - //=> { key: 'owner', kind: 'belongsTo', type: 'user', options: Object, isRelationship: true } - ``` - - @property relationshipsByName - @public - @static - @type Map - @readOnly - */ - @computeOnce - static get relationshipsByName() { - 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.`, - 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 - ); - } - let map = new Map(); - let rels = this.relationshipsObject; - let relationships = Object.keys(rels); - - for (let i = 0; i < relationships.length; i++) { - let key = relationships[i]; - let value = rels[key]; - - map.set(value.name || value.key, value); - } - - return map; - } - - @computeOnce - static get relationshipsObject() { - 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.`, - 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 - ); - } - let relationships = Object.create(null); - let modelName = this.modelName; - this.eachComputedProperty((name, meta) => { - if (meta.isRelationship) { - meta.key = name; - meta.name = name; - meta.parentModelName = modelName; - relationships[name] = DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE ? relationshipFromMeta(meta) : meta; - - assert( - `You should not specify both options.as and options.inverse as null on ${modelName}.${meta.name}, 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) - ); - } - }); - return relationships; - } - - /** - A map whose keys are the fields of the model and whose values are strings - describing the kind of the field. A model's fields are the union of all of its - attributes and relationships. - - For example: - - ```app/models/blog.js - import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - - export default class BlogModel extends Model { - @hasMany('user') users; - @belongsTo('user') owner; - - @hasMany('post') posts; - - @attr('string') title; - } - ``` - - ```js - import { get } from '@ember/object'; - import Blog from 'app/models/blog' - - let fields = Blog.fields; - fields.forEach(function(kind, field) { - // do thing - }); - - // prints: - // users, hasMany - // owner, belongsTo - // posts, hasMany - // title, attribute - ``` - - @property fields - @public - @static - @type Map - @readOnly - */ - @computeOnce - static get fields() { - 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.`, - 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 - ); - } - let map = new Map(); - - this.eachComputedProperty((name, meta) => { - // TODO end reliance on these booleans and stop leaking them in the spec - if (meta.isRelationship) { - map.set(name, meta.kind); - } else if (meta.isAttribute) { - map.set(name, 'attribute'); - } - }); - - return map; - } - - /** - Given a callback, iterates over each of the relationships in the model, - invoking the callback with the name of each relationship and its relationship - descriptor. - - @method eachRelationship - @public - @static - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - static eachRelationship(callback, binding) { - 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.`, - 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, relationship); - }); - } - - /** - Given a callback, iterates over each of the types related to a model, - invoking the callback with the related type's class. Each type will be - returned just once, regardless of how many different relationships it has - with a model. - - @method eachRelatedType - @public - @static - @param {Function} callback the callback to invoke - @param {any} binding the value to which the callback's `this` should be bound - */ - static eachRelatedType(callback, binding) { - 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.`, - 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 - ); - } - let relationshipTypes = this.relatedTypes; - - for (let i = 0; i < relationshipTypes.length; i++) { - let type = relationshipTypes[i]; - callback.call(binding, type); - } - } - - static determineRelationshipType(knownSide, store) { - 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.`, - 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 - ); - } - let knownKey = knownSide.key; - let knownKind = knownSide.kind; - let inverse = this.inverseFor(knownKey, store); - // let key; - let otherKind; - - if (!inverse) { - return knownKind === 'belongsTo' ? 'oneToNone' : 'manyToNone'; - } - - // key = inverse.name; - otherKind = inverse.kind; - - if (otherKind === 'belongsTo') { - return knownKind === 'belongsTo' ? 'oneToOne' : 'manyToOne'; - } else { - return knownKind === 'belongsTo' ? 'oneToMany' : 'manyToMany'; - } - } - - /** - A map whose keys are the attributes of the model (properties - described by attr) and whose values are the meta object for the - property. - - Example - - ```app/models/person.js - import Model, { attr } from '@ember-data/model'; - - export default class PersonModel extends Model { - @attr('string') firstName; - @attr('string') lastName; - @attr('date') birthday; - } - ``` - - ```javascript - import { get } from '@ember/object'; - import Person from 'app/models/person' - - let attributes = Person.attributes - - attributes.forEach(function(meta, name) { - // do thing - }); - - // prints: - // firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"} - // lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"} - // birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"} - ``` - - @property attributes - @public - @static - @type {Map} - @readOnly - */ - @computeOnce - static get attributes() { - 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.`, - 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 - ); - } - let map = new Map(); - - this.eachComputedProperty((name, meta) => { - if (meta.isAttribute) { - assert( - "You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: attr('')` from " + - this.toString(), - name !== 'id' - ); - - meta.name = name; - map.set(name, meta); - } - }); - - return map; - } - - /** - A map whose keys are the attributes of the model (properties - described by attr) and whose values are type of transformation - applied to each attribute. This map does not include any - attributes that do not have an transformation type. - - Example - - ```app/models/person.js - import Model, { attr } from '@ember-data/model'; - - export default class PersonModel extends Model { - @attr firstName; - @attr('string') lastName; - @attr('date') birthday; - } - ``` - - ```javascript - import { get } from '@ember/object'; - import Person from 'app/models/person'; - - let transformedAttributes = Person.transformedAttributes - - transformedAttributes.forEach(function(field, type) { - // do thing - }); - - // prints: - // lastName string - // birthday date - ``` - - @property transformedAttributes - @public - @static - @type {Map} - @readOnly - */ - @computeOnce - static get transformedAttributes() { - 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.`, - 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 - ); - } - let map = new Map(); - - this.eachAttribute((key, meta) => { - if (meta.type) { - map.set(key, meta.type); - } - }); - - return map; - } - - /** - Iterates through the attributes of the model, calling the passed function on each - attribute. - - The callback method you provide should have the following signature (all - parameters are optional): - - ```javascript - function(name, meta); - ``` - - - `name` the name of the current property in the iteration - - `meta` the meta object for the attribute property in the iteration - - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. - - Example - - ```javascript - import Model, { attr } from '@ember-data/model'; - - class PersonModel extends Model { - @attr('string') firstName; - @attr('string') lastName; - @attr('date') birthday; - } - - PersonModel.eachAttribute(function(name, meta) { - // do thing - }); - - // prints: - // firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"} - // lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"} - // birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"} - ``` - - @method eachAttribute - @public - @param {Function} callback The callback to execute - @param {Object} [binding] the value to which the callback's `this` should be bound - @static - */ - static eachAttribute(callback, binding) { - 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.`, - 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, meta); - }); - } - - /** - Iterates through the transformedAttributes of the model, calling - the passed function on each attribute. Note the callback will not be - called for any attributes that do not have an transformation type. - - The callback method you provide should have the following signature (all - parameters are optional): - - ```javascript - function(name, type); - ``` - - - `name` the name of the current property in the iteration - - `type` a string containing the name of the type of transformed - applied to the attribute - - Note that in addition to a callback, you can also pass an optional target - object that will be set as `this` on the context. - - Example - - ```javascript - import Model, { attr } from '@ember-data/model'; - - let Person = Model.extend({ - firstName: attr(), - lastName: attr('string'), - birthday: attr('date') - }); - - Person.eachTransformedAttribute(function(name, type) { - // do thing - }); - - // prints: - // lastName string - // birthday date - ``` - - @method eachTransformedAttribute - @public - @param {Function} callback The callback to execute - @param {Object} [binding] the value to which the callback's `this` should be bound - @static - */ - static eachTransformedAttribute(callback, binding) { - 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.`, - 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, name) => { - callback.call(binding, name, type); - }); - } - - /** - Returns the name of the model class. - - @method toString - @public - @static - */ - static toString() { - 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.`, - 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}`; - } -} - -// this is required to prevent `init` from passing -// the values initialized during create to `setUnknownProperty` -Model.prototype._createProps = null; -Model.prototype._secretInit = null; - -if (HAS_DEBUG_PACKAGE) { - /** - Provides info about the model for debugging purposes - by grouping the properties into more semantic groups. - - Meant to be used by debugging tools such as the Chrome Ember Extension. - - - Groups all attributes in "Attributes" group. - - Groups all belongsTo relationships in "Belongs To" group. - - Groups all hasMany relationships in "Has Many" group. - - Groups all flags in "Flags" group. - - Flags relationship CPs as expensive properties. - - @method _debugInfo - @for Model - @private - */ - Model.prototype._debugInfo = function () { - let relationships = {}; - let expensiveProperties = []; - - const identifier = recordIdentifierFor(this); - const schema = this.store.getSchemaDefinitionService(); - const attrDefs = schema.attributesDefinitionFor(identifier); - const relDefs = schema.relationshipsDefinitionFor(identifier); - - const attributes = Object.keys(attrDefs); - attributes.unshift('id'); - - let groups = [ - { - name: 'Attributes', - properties: attributes, - expand: true, - }, - ]; - - Object.keys(relDefs).forEach((name) => { - const relationship = relDefs[name]; - - let properties = relationships[relationship.kind]; - - if (properties === undefined) { - properties = relationships[relationship.kind] = []; - groups.push({ - name: relationship.kind, - properties, - expand: true, - }); - } - properties.push(name); - expensiveProperties.push(name); - }); - - groups.push({ - name: 'Flags', - properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], - }); - - return { - propertyInfo: { - // include all other mixins / properties (not just the grouped ones) - includeOtherProperties: true, - groups: groups, - // don't pre-calculate unless cached - expensiveProperties: expensiveProperties, - }, - }; - }; -} - -if (DEBUG) { - let lookupDescriptor = function lookupDescriptor(obj, keyName) { - let current = obj; - do { - let descriptor = Object.getOwnPropertyDescriptor(current, keyName); - if (descriptor !== undefined) { - return descriptor; - } - current = Object.getPrototypeOf(current); - } while (current !== null); - return null; - }; - - Model.reopen({ - init() { - this._super(...arguments); - - let ourDescriptor = lookupDescriptor(Model.prototype, 'currentState'); - let theirDescriptor = lookupDescriptor(this, 'currentState'); - let realState = this.___recordState; - if (ourDescriptor.get !== theirDescriptor.get || realState !== this.currentState) { - throw new Error( - `'currentState' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` - ); - } - - const ID_DESCRIPTOR = lookupDescriptor(Model.prototype, 'id'); - let idDesc = lookupDescriptor(this, 'id'); - - if (idDesc.get !== ID_DESCRIPTOR.get) { - throw new Error( - `You may not set 'id' as an attribute on your model. Please remove any lines that look like: \`id: attr('')\` from ${this.constructor.toString()}` - ); - } - }, - }); - - if (DEPRECATE_MODEL_REOPEN) { - const originalReopen = Model.reopen; - const originalReopenClass = Model.reopenClass; - - 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); - }; - - 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); - }; - } -} - -export default Model; diff --git a/packages/model/src/-private/model.ts b/packages/model/src/-private/model.ts new file mode 100644 index 00000000000..ea2eff41537 --- /dev/null +++ b/packages/model/src/-private/model.ts @@ -0,0 +1,2529 @@ +/** + @module @ember-data/model + */ + +import { deprecate, warn } from '@ember/debug'; +import EmberObject from '@ember/object'; + +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import type Store from '@ember-data/store'; +import type { NotificationType } from '@ember-data/store'; +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'; +import type { Cache, ChangedAttributesHash } from '@warp-drive/core-types/cache'; +import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; +import { RecordStore } from '@warp-drive/core-types/symbols'; + +import { Errors } from './errors'; +import { LEGACY_SUPPORT } from './legacy-relationships-support'; +import type { MinimalLegacyRecord } from './model-methods'; +import { + belongsTo, + changedAttributes, + createSnapshot, + deleteRecord, + destroyRecord, + hasMany, + reload, + rollbackAttributes, + save, + serialize, + unloadRecord, +} from './model-methods'; +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, + MaybeAttrFields, + MaybeHasManyFields, + MaybeRelationshipFields, +} from './type-utils'; + +export type ModelCreateArgs = { + _createProps: Record; + // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for + _secretInit: { + identifier: StableRecordIdentifier; + cache: Cache; + store: Store; + cb: (record: Model, cache: Cache, identifier: StableRecordIdentifier, store: Store) => void; + }; +}; + +export type StaticModel = typeof Model & { create(options: ModelCreateArgs): Model }; +export type ModelFactory = { class: StaticModel }; +export type FactoryCache = Record; +// we put this on the store for interop because it's used by modelFor and +// instantiateRecord as well. +export type ModelStore = Store & { _modelFactoryCache: FactoryCache }; + +/* + * This decorator allows us to lazily compute + * an expensive getter on first-access and thereafter + * never recompute it. + */ +function computeOnce(target: object, propertyName: string, desc: PropertyDescriptor) { + const cache = new WeakMap(); + // eslint-disable-next-line @typescript-eslint/unbound-method + const getter = desc.get; + desc.get = function () { + let meta = cache.get(this); + + if (!meta) { + meta = { hasComputed: false, value: undefined }; + cache.set(this, meta); + } + + if (!meta.hasComputed) { + meta.value = (getter as () => unknown).call(this); + meta.hasComputed = true; + } + + return meta.value; + }; + return desc; +} + +/** + Base class from which Models can be defined. + + ```js + import Model, { attr } from '@ember-data/model'; + + export default class User extends Model { + @attr name; + } + ``` + + Models are used both to define the static schema for a + particular resource type as well as the class to instantiate + to present that data from cache. + + @class Model + @public + @extends Ember.EmberObject +*/ + +interface Model { + serialize(this: T, options?: Record): unknown; + destroyRecord(this: T, options?: Record): Promise; + unloadRecord(this: T): void; + changedAttributes(this: T): ChangedAttributesHash; + rollbackAttributes(this: T): void; + _createSnapshot(this: T): Snapshot; + save(this: T, options?: Record): Promise; + reload(this: T, options?: Record): Promise; + + // belongsTo>( + // this: T, + // prop: K + // ): BelongsToReference; + belongsTo( + this: T, + prop: K & (K extends _MaybeBelongsToFields ? K : never) + ): BelongsToReference; + hasMany>(this: T, prop: K): HasManyReference; + deleteRecord(this: T): void; +} +class Model extends EmberObject implements MinimalLegacyRecord { + // set during create by the store + declare store: Store; + declare ___recordState: RecordState; + declare ___private_notifications: object; + declare [RecordStore]: Store; + + init(options: ModelCreateArgs) { + if (DEBUG) { + if (!options?._secretInit && !options?._createProps) { + throw new Error( + 'You should not call `create` on a model. Instead, call `store.createRecord` with the attributes you would like to set.' + ); + } + } + const createProps = options._createProps; + const _secretInit = options._secretInit; + (options as Record)._createProps = null; + (options as Record)._secretInit = null; + + const store = (this.store = _secretInit.store); + super.init(options); + + this[RecordStore] = store; + + const identity = _secretInit.identifier; + _secretInit.cb(this, _secretInit.cache, identity, _secretInit.store); + + this.___recordState = DEBUG + ? new RecordState(this as unknown as MinimalLegacyRecord) + : (null as unknown as RecordState); + + this.setProperties(createProps); + + const notifications = store.notifications; + this.___private_notifications = notifications.subscribe( + identity, + (identifier: StableRecordIdentifier, type: NotificationType, field?: string): void => { + notifyChanges(identifier, type, field, this, store); + } + ); + } + + // @ts-expect-error destroy should not return a value, but ember's types force it to + destroy(): this { + const identifier = recordIdentifierFor(this); + this.___recordState?.destroy(); + const store = storeFor(this)!; + store.notifications.unsubscribe(this.___private_notifications); + // Legacy behavior is to notify the relationships on destroy + // such that they "clear". It's uncertain this behavior would + // be good for a new model paradigm, likely cheaper and safer + // to simply not notify, for this reason the store does not itself + // notify individual changes once the delete has been signaled, + // this decision is left to model instances. + + this.eachRelationship((name, meta) => { + if (meta.kind === 'belongsTo') { + this.notifyPropertyChange(name); + } + }); + LEGACY_SUPPORT.get(this as unknown as MinimalLegacyRecord)?.destroy(); + LEGACY_SUPPORT.delete(this as unknown as MinimalLegacyRecord); + LEGACY_SUPPORT.delete(identifier); + + super.destroy(); + } + + /** + If this property is `true` the record is in the `empty` + state. Empty is the first state all records enter after they have + been created. Most records created by the store will quickly + transition to the `loading` state if data needs to be fetched from + the server or the `created` state if the record is created on the + client. A record can also enter the empty state if the adapter is + unable to locate the record. + + @property isEmpty + @public + @type {Boolean} + @readOnly + */ + @compat + get isEmpty(): boolean { + return this.currentState.isEmpty; + } + + /** + If this property is `true` the record is in the `loading` state. A + record enters this state when the store asks the adapter for its + data. It remains in this state until the adapter provides the + requested data. + + @property isLoading + @public + @type {Boolean} + @readOnly + */ + @compat + get isLoading(): boolean { + return this.currentState.isLoading; + } + + /** + If this property is `true` the record is in the `loaded` state. A + record enters this state when its data is populated. Most of a + record's lifecycle is spent inside substates of the `loaded` + state. + + Example + + ```javascript + let record = store.createRecord('model'); + record.isLoaded; // true + + store.findRecord('model', 1).then(function(model) { + model.isLoaded; // true + }); + ``` + + @property isLoaded + @public + @type {Boolean} + @readOnly + */ + @compat + get isLoaded(): boolean { + return this.currentState.isLoaded; + } + + /** + If this property is `true` the record is in the `dirty` state. The + record has local changes that have not yet been saved by the + adapter. This includes records that have been created (but not yet + saved) or deleted. + + Example + + ```javascript + let record = store.createRecord('model'); + record.hasDirtyAttributes; // true + + store.findRecord('model', 1).then(function(model) { + model.hasDirtyAttributes; // false + model.set('foo', 'some value'); + model.hasDirtyAttributes; // true + }); + ``` + + @since 1.13.0 + @property hasDirtyAttributes + @public + @type {Boolean} + @readOnly + */ + @compat + get hasDirtyAttributes(): boolean { + return this.currentState.isDirty; + } + + /** + If this property is `true` the record is in the `saving` state. A + record enters the saving state when `save` is called, but the + adapter has not yet acknowledged that the changes have been + persisted to the backend. + + Example + + ```javascript + let record = store.createRecord('model'); + record.isSaving; // false + let promise = record.save(); + record.isSaving; // true + promise.then(function() { + record.isSaving; // false + }); + ``` + + @property isSaving + @public + @type {Boolean} + @readOnly + */ + @compat + get isSaving(): boolean { + return this.currentState.isSaving; + } + + /** + If this property is `true` the record is in the `deleted` state + and has been marked for deletion. When `isDeleted` is true and + `hasDirtyAttributes` is true, the record is deleted locally but the deletion + was not yet persisted. When `isSaving` is true, the change is + in-flight. When both `hasDirtyAttributes` and `isSaving` are false, the + change has persisted. + + Example + + ```javascript + let record = store.createRecord('model'); + record.isDeleted; // false + record.deleteRecord(); + + // Locally deleted + record.isDeleted; // true + record.hasDirtyAttributes; // true + record.isSaving; // false + + // Persisting the deletion + let promise = record.save(); + record.isDeleted; // true + record.isSaving; // true + + // Deletion Persisted + promise.then(function() { + record.isDeleted; // true + record.isSaving; // false + record.hasDirtyAttributes; // false + }); + ``` + + @property isDeleted + @public + @type {Boolean} + @readOnly + */ + @compat + get isDeleted(): boolean { + return this.currentState.isDeleted; + } + + /** + If this property is `true` the record is in the `new` state. A + record will be in the `new` state when it has been created on the + client and the adapter has not yet report that it was successfully + saved. + + Example + + ```javascript + let record = store.createRecord('model'); + record.isNew; // true + + record.save().then(function(model) { + model.isNew; // false + }); + ``` + + @property isNew + @public + @type {Boolean} + @readOnly + */ + @compat + get isNew(): boolean { + return this.currentState.isNew; + } + + /** + If this property is `true` the record is in the `valid` state. + + A record will be in the `valid` state when the adapter did not report any + server-side validation failures. + + @property isValid + @public + @type {Boolean} + @readOnly + */ + @compat + get isValid(): boolean { + return this.currentState.isValid; + } + + /** + If the record is in the dirty state this property will report what + kind of change has caused it to move into the dirty + state. Possible values are: + + - `created` The record has been created by the client and not yet saved to the adapter. + - `updated` The record has been updated by the client and not yet saved to the adapter. + - `deleted` The record has been deleted by the client and not yet saved to the adapter. + + Example + + ```javascript + let record = store.createRecord('model'); + record.dirtyType; // 'created' + ``` + + @property dirtyType + @public + @type {String} + @readOnly + */ + @compat + get dirtyType(): 'created' | 'updated' | 'deleted' | '' { + return this.currentState.dirtyType; + } + + /** + If `true` the adapter reported that it was unable to save local + changes to the backend for any reason other than a server-side + validation error. + + Example + + ```javascript + record.isError; // false + record.set('foo', 'valid value'); + record.save().then(null, function() { + record.isError; // true + }); + ``` + + @property isError + @public + @type {Boolean} + @readOnly + */ + @compat + get isError(): boolean { + return this.currentState.isError; + } + set isError(v) { + if (DEBUG) { + throw new Error(`isError is not directly settable`); + } + } + + /** + If `true` the store is attempting to reload the record from the adapter. + + Example + + ```javascript + record.isReloading; // false + record.reload(); + record.isReloading; // true + ``` + + @property isReloading + @public + @type {Boolean} + @readOnly + */ + declare isReloading: boolean; + + /** + All ember models have an id property. This is an identifier + managed by an external source. These are always coerced to be + strings before being used internally. Note when declaring the + attributes for a model it is an error to declare an id + attribute. + + ```javascript + let record = store.createRecord('model'); + record.id; // null + + store.findRecord('model', 1).then(function(model) { + model.id; // '1' + }); + ``` + + @property id + @public + @type {String} + */ + @tagged + get id(): string | null { + // this guard exists, because some dev-only deprecation code + // (addListener via validatePropertyInjections) invokes toString before the + // object is real. + if (DEBUG) { + try { + return recordIdentifierFor(this).id; + } catch { + return null; + } + } + return recordIdentifierFor(this).id; + } + set id(id) { + const normalizedId = coerceId(id); + const identifier = recordIdentifierFor(this); + const didChange = normalizedId !== identifier.id; + assert( + `Cannot set ${identifier.type} record's id to ${id}, because id is already ${identifier.id}`, + !didChange || identifier.id === null + ); + + if (normalizedId !== null && didChange) { + this.store._instanceCache.setRecordId(identifier, normalizedId); + this.store.notifications.notify(identifier, 'identity'); + } + } + + toString() { + return ``; + } + + /** + @property currentState + @private + @type {Object} + */ + // TODO we can probably make this a computeOnce + // we likely do not need to notify the currentState root anymore + @tagged + get currentState() { + // descriptors are called with the wrong `this` context during mergeMixins + // when using legacy/classic ember classes. Basically: lazy in prod and eager in dev. + // so we do this to try to steer folks to the nicer "dont user currentState" + // error. + if (!DEBUG) { + if (!this.___recordState) { + this.___recordState = new RecordState(this as unknown as MinimalLegacyRecord); + } + } + return this.___recordState; + } + set currentState(_v) { + throw new Error('cannot set currentState'); + } + + /** + The store service instance which created this record instance + + @property store + @public + */ + + /** + When the record is in the `invalid` state this object will contain + any errors returned by the adapter. When present the errors hash + contains keys corresponding to the invalid property names + and values which are arrays of Javascript objects with two keys: + + - `message` A string containing the error message from the backend + - `attribute` The name of the property associated with this error message + + ```javascript + record.errors.length; // 0 + record.set('foo', 'invalid value'); + record.save().catch(function() { + record.errors.foo; + // [{message: 'foo should be a number.', attribute: 'foo'}] + }); + ``` + + The `errors` property is useful for displaying error messages to + the user. + + ```handlebars + + {{#each @model.errors.username as |error|}} +
+ {{error.message}} +
+ {{/each}} + + {{#each @model.errors.email as |error|}} +
+ {{error.message}} +
+ {{/each}} + ``` + + + You can also access the special `messages` property on the error + object to get an array of all the error strings. + + ```handlebars + {{#each @model.errors.messages as |message|}} +
+ {{message}} +
+ {{/each}} + ``` + + @property errors + @public + @type {Errors} + */ + @computeOnce + get errors(): Errors { + const errors = (Errors as unknown as { create(obj: object): Errors }).create({ __record: this }); + this.currentState.updateInvalidErrors(errors); + return errors; + } + + /** + This property holds the `AdapterError` object with which + last adapter operation was rejected. + + @property adapterError + @public + @type {AdapterError} + */ + @compat + get adapterError() { + return this.currentState.adapterError; + } + set adapterError(v) { + throw new Error(`adapterError is not directly settable`); + } + + /** + Create a JSON representation of the record, using the serialization + strategy of the store's adapter. + + `serialize` takes an optional hash as a parameter, currently + supported options are: + + - `includeId`: `true` if the record's ID should be included in the + JSON representation. + + @method serialize + @public + @param {Object} options + @return {Object} an object whose values are primitive JSON values only + */ + /* + We hook the default implementation to ensure + our tagged properties are properly notified + as well. We still super for everything because + sync observers require a direct call occuring + to trigger their flush. We wouldn't need to + super in 4.0+ where sync observers are removed. + */ + // @ts-expect-error no return is necessary, but Ember's types are forcing it + notifyPropertyChange(prop: string): this { + notifySignal(this, prop as keyof this & string); + super.notifyPropertyChange(prop); + } + + /** + Marks the record as deleted but does not save it. You must call + `save` afterwards if you want to persist it. You might use this + method if you want to allow the user to still `rollbackAttributes()` + after a delete was made. + + Example + + ```js + import Component from '@glimmer/component'; + + export default class extends Component { + softDelete = () => { + this.args.model.deleteRecord(); + } + + confirm = () => { + this.args.model.save(); + } + + undo = () => { + this.args.model.rollbackAttributes(); + } + } + ``` + + @method deleteRecord + @public + */ + + /** + Same as `deleteRecord`, but saves the record immediately. + + Example + + ```js + import Component from '@glimmer/component'; + + export default class extends Component { + delete = () => { + this.args.model.destroyRecord().then(function() { + this.transitionToRoute('model.index'); + }); + } + } + ``` + + If you pass an object on the `adapterOptions` property of the options + argument it will be passed to your adapter via the snapshot + + ```js + record.destroyRecord({ adapterOptions: { subscribe: false } }); + ``` + + ```app/adapters/post.js + import MyCustomAdapter from './custom-adapter'; + + export default class PostAdapter extends MyCustomAdapter { + deleteRecord(store, type, snapshot) { + if (snapshot.adapterOptions.subscribe) { + // ... + } + // ... + } + } + ``` + + @method destroyRecord + @public + @param {Object} options + @return {Promise} a promise that will be resolved when the adapter returns + successfully or rejected if the adapter returns with an error. + */ + + /** + Unloads the record from the store. This will not send a delete request + to your server, it just unloads the record from memory. + + @method unloadRecord + @public + */ + + /** + Returns an object, whose keys are changed properties, and value is + an [oldProp, newProp] array. + + The array represents the diff of the canonical state with the local state + of the model. Note: if the model is created locally, the canonical state is + empty since the adapter hasn't acknowledged the attributes yet: + + Example + + ```app/models/mascot.js + import Model, { attr } from '@ember-data/model'; + + export default class MascotModel extends Model { + @attr('string') name; + @attr('boolean', { + defaultValue: false + }) + isAdmin; + } + ``` + + ```javascript + let mascot = store.createRecord('mascot'); + + mascot.changedAttributes(); // {} + + mascot.set('name', 'Tomster'); + mascot.changedAttributes(); // { name: [undefined, 'Tomster'] } + + mascot.set('isAdmin', true); + mascot.changedAttributes(); // { isAdmin: [undefined, true], name: [undefined, 'Tomster'] } + + mascot.save().then(function() { + mascot.changedAttributes(); // {} + + mascot.set('isAdmin', false); + mascot.changedAttributes(); // { isAdmin: [true, false] } + }); + ``` + + @method changedAttributes + @public + @return {Object} an object, whose keys are changed properties, + and value is an [oldProp, newProp] array. + */ + + /** + If the model `hasDirtyAttributes` this function will discard any unsaved + changes. If the model `isNew` it will be removed from the store. + + Example + + ```javascript + record.name; // 'Untitled Document' + record.set('name', 'Doc 1'); + record.name; // 'Doc 1' + record.rollbackAttributes(); + record.name; // 'Untitled Document' + ``` + + @since 1.13.0 + @method rollbackAttributes + @public + */ + + /** + @method _createSnapshot + @private + */ + // TODO @deprecate in favor of a public API or examples of how to test successfully + + /** + Save the record and persist any changes to the record to an + external source via the adapter. + + Example + + ```javascript + record.set('name', 'Tomster'); + record.save().then(function() { + // Success callback + }, function() { + // Error callback + }); + ``` + + If you pass an object using the `adapterOptions` property of the options + argument it will be passed to your adapter via the snapshot. + + ```js + record.save({ adapterOptions: { subscribe: false } }); + ``` + + ```app/adapters/post.js + import MyCustomAdapter from './custom-adapter'; + + export default class PostAdapter extends MyCustomAdapter { + updateRecord(store, type, snapshot) { + if (snapshot.adapterOptions.subscribe) { + // ... + } + // ... + } + } + ``` + + @method save + @public + @param {Object} options + @return {Promise} a promise that will be resolved when the adapter returns + successfully or rejected if the adapter returns with an error. + */ + + /** + Reload the record from the adapter. + + This will only work if the record has already finished loading. + + Example + + ```js + import Component from '@glimmer/component'; + + export default class extends Component { + async reload = () => { + await this.args.model.reload(); + // do something with the reloaded model + } + } + ``` + + @method reload + @public + @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter request + + @return {Promise} a promise that will be resolved with the record when the + adapter returns successfully or rejected if the adapter returns + with an error. + */ + + attr() { + assert( + 'The `attr` method is not available on Model, a Snapshot was probably expected. Are you passing a Model instead of a Snapshot to your serializer?', + false + ); + } + + /** + Get the reference for the specified belongsTo relationship. + + For instance, given the following model + + ```app/models/blog-post.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class BlogPost extends Model { + @belongsTo('user', { async: true, inverse: null }) author; + } + ``` + + Then the reference for the author relationship would be + retrieved from a record instance like so: + + ```js + blogPost.belongsTo('author'); + ``` + + A `BelongsToReference` is a low-level API that allows access + and manipulation of a belongsTo relationship. + + It is especially useful when you're dealing with `async` relationships + as it allows synchronous access to the relationship data if loaded, as + well as APIs for loading, reloading the data or accessing available + information without triggering a load. + + It may also be useful when using `sync` relationships that need to be + loaded/reloaded with more precise timing than marking the + relationship as `async` and relying on autofetch would have allowed. + + However,keep in mind that marking a relationship as `async: false` will introduce + bugs into your application if the data is not always guaranteed to be available + by the time the relationship is accessed. Ergo, it is recommended when using this + approach to utilize `links` for unloaded relationship state instead of identifiers. + + Reference APIs are entangled with the relationship's underlying state, + thus any getters or cached properties that utilize these will properly + invalidate if the relationship state changes. + + References are "stable", meaning that multiple calls to retrieve the reference + for a given relationship will always return the same HasManyReference. + + @method belongsTo + @public + @param {String} name of the relationship + @since 2.5.0 + @return {BelongsToReference} reference for this relationship + */ + + /** + Get the reference for the specified hasMany relationship. + + For instance, given the following model + + ```app/models/blog-post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class BlogPost extends Model { + @hasMany('comment', { async: true, inverse: null }) comments; + } + ``` + + Then the reference for the comments relationship would be + retrieved from a record instance like so: + + ```js + blogPost.hasMany('comments'); + ``` + + A `HasManyReference` is a low-level API that allows access + and manipulation of a hasMany relationship. + + It is especially useful when you are dealing with `async` relationships + as it allows synchronous access to the relationship data if loaded, as + well as APIs for loading, reloading the data or accessing available + information without triggering a load. + + It may also be useful when using `sync` relationships with `@ember-data/model` + that need to be loaded/reloaded with more precise timing than marking the + relationship as `async` and relying on autofetch would have allowed. + + However,keep in mind that marking a relationship as `async: false` will introduce + bugs into your application if the data is not always guaranteed to be available + by the time the relationship is accessed. Ergo, it is recommended when using this + approach to utilize `links` for unloaded relationship state instead of identifiers. + + Reference APIs are entangled with the relationship's underlying state, + thus any getters or cached properties that utilize these will properly + invalidate if the relationship state changes. + + References are "stable", meaning that multiple calls to retrieve the reference + for a given relationship will always return the same HasManyReference. + + @method hasMany + @public + @param {String} name of the relationship + @since 2.5.0 + @return {HasManyReference} reference for this relationship + */ + + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(name, descriptor); + ``` + + - `name` the name of the current property in the iteration + - `descriptor` the meta object that describes this relationship + + The relationship descriptor argument is an object with the following properties. + + - **name** String the name of this relationship on the Model + - **kind** String "hasMany" or "belongsTo" + - **options** Object the original options hash passed when the relationship was declared + - **parentType** Model the type of the Model that owns this relationship + - **type** String the type name of the related Model + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```app/serializers/application.js + import JSONSerializer from '@ember-data/serializer/json'; + + export default class ApplicationSerializer extends JSONSerializer { + serialize(record, options) { + let json = {}; + + record.eachRelationship(function(name, descriptor) { + if (descriptor.kind === 'hasMany') { + let serializedHasManyName = name.toUpperCase() + '_IDS'; + json[serializedHasManyName] = record.get(name).map(r => r.id); + } + }); + + return json; + } + } + ``` + + @method eachRelationship + @public + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + eachRelationship( + callback: ( + this: NoInfer | undefined, + key: MaybeRelationshipFields, + meta: LegacyRelationshipSchema + ) => void, + binding?: T + ): void { + (this.constructor as typeof Model).eachRelationship(callback, binding); + } + + relationshipFor(name: string): LegacyRelationshipSchema | undefined { + return (this.constructor as typeof Model).relationshipsByName.get(name); + } + + inverseFor(name: string) { + return (this.constructor as typeof Model).inverseFor(name, storeFor(this)!); + } + + eachAttribute( + callback: ( + this: NoInfer | undefined, + key: isSubClass extends true ? MaybeAttrFields : string, + meta: LegacyAttributeField + ) => void, + binding?: T + ): void { + (this.constructor as typeof Model).eachAttribute(callback, binding); + } + + static isModel = true; + + /** + Create should only ever be called by the store. To create an instance of a + `Model` in a dirty state use `store.createRecord`. + + To create instances of `Model` in a clean state, use `store.push` + + @method create + @private + @static + */ + + /** + Represents the model's class name as a string. This can be used to look up the model's class name through + `Store`'s modelFor method. + + `modelName` is generated for you by EmberData. It will be a lowercased, dasherized string. + For example: + + ```javascript + store.modelFor('post').modelName; // 'post' + store.modelFor('blog-post').modelName; // 'blog-post' + ``` + + The most common place you'll want to access `modelName` is in your serializer's `payloadKeyFromModelName` method. For example, to change payload + keys to underscore (instead of dasherized), you might use the following code: + + ```javascript + import RESTSerializer from '@ember-data/serializer/rest'; + import { underscore } from '/utils/string-utils'; + + export default const PostSerializer = RESTSerializer.extend({ + payloadKeyFromModelName(modelName) { + return underscore(modelName); + } + }); + ``` + @property modelName + @public + @type String + @readonly + @static + */ + static modelName: string = null as unknown as string; + + /* + These class methods below provide relationship + introspection abilities about relationships. + + A note about the computed properties contained here: + + **These properties are effectively sealed once called for the first time.** + To avoid repeatedly doing expensive iteration over a model's fields, these + values are computed once and then cached for the remainder of the runtime of + your application. + + If your application needs to modify a class after its initial definition + (for example, using `reopen()` to add additional attributes), make sure you + do it before using your model with the store, which uses these properties + extensively. + */ + + /** + For a given relationship name, returns the model type of the relationship. + + For example, if you define a model like this: + + ```app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class PostModel extends Model { + @hasMany('comment') comments; + } + ``` + + Calling `store.modelFor('post').typeForRelationship('comments', store)` will return `Comment`. + + @method typeForRelationship + @public + @static + @param {String} name the name of the relationship + @param {store} store an instance of Store + @return {Model} the type of the relationship, or undefined + */ + 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 { + 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; + } + + /** + Find the relationship which is the inverse of the one asked for. + + For example, if you define models like this: + + ```app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class PostModel extends Model { + @hasMany('message') comments; + } + ``` + + ```app/models/message.js + import Model, { belongsTo } from '@ember-data/model'; + + export default class MessageModel extends Model { + @belongsTo('post') owner; + } + ``` + + ``` js + store.modelFor('post').inverseFor('comments', store) // { type: 'message', name: 'owner', kind: 'belongsTo' } + store.modelFor('message').inverseFor('owner', store) // { type: 'post', name: 'comments', kind: 'hasMany' } + ``` + + @method inverseFor + @public + @static + @param {String} name the name of the relationship + @param {Store} store + @return {Object} the inverse relationship, or null + */ + static inverseFor(name: string, store: Store): LegacyRelationshipSchema | null { + 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]; + } else { + const inverse = this._findInverseFor(name, store); + inverseMap[name] = inverse; + return inverse; + } + } + + //Calculate the inverse, ignoring the cache + static _findInverseFor(name: string, store: Store): LegacyRelationshipSchema | null { + 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); + + if (!relationship) { + return null; + } + + const { options } = relationship; + assert( + `Expected the relationship ${name} on ${this.modelName} to define an inverse.`, + options.inverse === null || (typeof options.inverse === 'string' && options.inverse.length > 0) + ); + + if (options.inverse === null) { + return null; + } + + const schemaExists = store.schema.hasResource(relationship); + + assert( + `No associated schema found for '${relationship.type}' while calculating the inverse of ${name} on ${this.modelName}`, + schemaExists + ); + + if (!schemaExists) { + return null; + } + + const inverseField = store.schema.fields(relationship).get(options.inverse); + assert( + `No inverse relationship found for '${name}' on '${this.modelName}'`, + inverseField && (inverseField.kind === 'belongsTo' || inverseField.kind === 'hasMany') + ); + + return inverseField || null; + } + + /** + The model's relationships as a map, keyed on the type of the + relationship. The value of each entry is an array containing a descriptor + for each relationship with that type, describing the name of the relationship + as well as the type. + + For example, given the following model definition: + + ```app/models/blog.js + import Model, { belongsTo, hasMany } from '@ember-data/model'; + + export default class BlogModel extends Model { + @hasMany('user') users; + @belongsTo('user') owner; + @hasMany('post') posts; + } + ``` + + This computed property would return a map describing these + relationships, like this: + + ```javascript + import Blog from 'app/models/blog'; + import User from 'app/models/user'; + import Post from 'app/models/post'; + + let relationships = Blog.relationships; + relationships.user; + //=> [ { name: 'users', kind: 'hasMany' }, + // { name: 'owner', kind: 'belongsTo' } ] + relationships.post; + //=> [ { name: 'posts', kind: 'hasMany' } ] + ``` + + @property relationships + @public + @static + @type Map + @readOnly + */ + + @computeOnce + static get relationships(): Map { + 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; + + // Loop through each computed property on the class + relationshipsByName.forEach((desc) => { + const { type } = desc; + + if (!map.has(type)) { + map.set(type, []); + } + + map.get(type)!.push(desc); + }); + + return map; + } + + /** + A hash containing lists of the model's relationships, grouped + by the relationship kind. For example, given a model with this + definition: + + ```app/models/blog.js + import Model, { belongsTo, hasMany } from '@ember-data/model'; + + export default class BlogModel extends Model { + @hasMany('user') users; + @belongsTo('user') owner; + + @hasMany('post') posts; + } + ``` + + This property would contain the following: + + ```javascript + import Blog from 'app/models/blog'; + + let relationshipNames = Blog.relationshipNames; + relationshipNames.hasMany; + //=> ['users', 'posts'] + relationshipNames.belongsTo; + //=> ['owner'] + ``` + + @property relationshipNames + @public + @static + @type Object + @readOnly + */ + @computeOnce + static get relationshipNames() { + 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: [], + }; + + this.eachComputedProperty((name, meta) => { + if (isRelationshipSchema(meta)) { + names[meta.kind].push(name); + } + }); + + return names; + } + + /** + An array of types directly related to a model. Each type will be + included once, regardless of the number of relationships it has with + the model. + + For example, given a model with this definition: + + ```app/models/blog.js + import Model, { belongsTo, hasMany } from '@ember-data/model'; + + export default class BlogModel extends Model { + @hasMany('user') users; + @belongsTo('user') owner; + + @hasMany('post') posts; + } + ``` + + This property would contain the following: + + ```javascript + import Blog from 'app/models/blog'; + + let relatedTypes = Blog.relatedTypes'); + //=> ['user', 'post'] + ``` + + @property relatedTypes + @public + @static + @type Array + @readOnly + */ + @computeOnce + static get relatedTypes(): string[] { + 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[] = []; + + const rels = this.relationshipsObject; + const relationships = Object.keys(rels); + + // create an array of the unique types involved + // in relationships + for (let i = 0; i < relationships.length; i++) { + const name = relationships[i]; + const meta = rels[name]; + const modelName = meta.type; + + if (!types.includes(modelName)) { + types.push(modelName); + } + } + + return types; + } + + /** + A map whose keys are the relationships of a model and whose values are + relationship descriptors. + + For example, given a model with this + definition: + + ```app/models/blog.js + import Model, { belongsTo, hasMany } from '@ember-data/model'; + + export default class BlogModel extends Model { + @hasMany('user') users; + @belongsTo('user') owner; + + @hasMany('post') posts; + } + ``` + + This property would contain the following: + + ```javascript + import Blog from 'app/models/blog'; + + let relationshipsByName = Blog.relationshipsByName; + relationshipsByName.users; + //=> { name: 'users', kind: 'hasMany', type: 'user', options: Object } + relationshipsByName.owner; + //=> { name: 'owner', kind: 'belongsTo', type: 'user', options: Object } + ``` + + @property relationshipsByName + @public + @static + @type Map + @readOnly + */ + @computeOnce + static get relationshipsByName(): Map { + 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); + + for (let i = 0; i < relationships.length; i++) { + const name = relationships[i]; + const value = rels[name]; + + map.set(value.name, value); + } + + return map; + } + + @computeOnce + static get relationshipsObject(): Record { + 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; + this.eachComputedProperty((name: string, meta: unknown) => { + if (!isRelationshipSchema(meta)) { + return; + } + // TODO deprecate key being here + (meta as unknown as { key: string }).key = name; + meta.name = name; + 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( + `You should not specify both options.as and options.inverse as null on ${modelName}.${meta.name}, 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) + ); + }); + return relationships; + } + + /** + A map whose keys are the fields of the model and whose values are strings + describing the kind of the field. A model's fields are the union of all of its + attributes and relationships. + + For example: + + ```app/models/blog.js + import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; + + export default class BlogModel extends Model { + @hasMany('user') users; + @belongsTo('user') owner; + + @hasMany('post') posts; + + @attr('string') title; + } + ``` + + ```js + import Blog from 'app/models/blog' + + let fields = Blog.fields; + fields.forEach(function(kind, field) { + // do thing + }); + + // prints: + // users, hasMany + // owner, belongsTo + // posts, hasMany + // title, attribute + ``` + + @property fields + @public + @static + @type Map + @readOnly + */ + @computeOnce + static get fields(): Map { + 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) => { + if (isRelationshipSchema(meta)) { + map.set(name, meta.kind); + } else if (isAttributeSchema(meta)) { + map.set(name, 'attribute'); + } + }); + + return map; + } + + /** + Given a callback, iterates over each of the relationships in the model, + invoking the callback with the name of each relationship and its relationship + descriptor. + + @method eachRelationship + @public + @static + @param {Function} callback the callback to invoke + @param {any} binding the value to which the callback's `this` should be bound + */ + static eachRelationship( + callback: ( + this: T | undefined, + key: MaybeRelationshipFields, + relationship: LegacyRelationshipSchema + ) => void, + binding?: T + ): void { + 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); + }); + } + + /** + Given a callback, iterates over each of the types related to a model, + invoking the callback with the related type's class. Each type will be + returned just once, regardless of how many different relationships it has + with a model. + + @method eachRelatedType + @public + @static + @param {Function} callback the callback to invoke + @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) { + 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; + + for (let i = 0; i < relationshipTypes.length; i++) { + const type = relationshipTypes[i]; + callback.call(binding, type); + } + } + + /** + * + * @method determineRelationshipType + * @private + * @deprecated + */ + static determineRelationshipType( + knownSide: LegacyRelationshipSchema, + store: Store + ): 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany' | 'oneToNone' | 'manyToNone' { + 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; + const inverse = this.inverseFor(knownKey, store); + // let key; + + if (!inverse) { + return knownKind === 'belongsTo' ? 'oneToNone' : 'manyToNone'; + } + + // key = inverse.name; + const otherKind = inverse.kind; + + if (otherKind === 'belongsTo') { + return knownKind === 'belongsTo' ? 'oneToOne' : 'manyToOne'; + } else { + return knownKind === 'belongsTo' ? 'oneToMany' : 'manyToMany'; + } + } + + /** + A map whose keys are the attributes of the model (properties + described by attr) and whose values are the meta object for the + property. + + Example + + ```app/models/person.js + import Model, { attr } from '@ember-data/model'; + + export default class PersonModel extends Model { + @attr('string') firstName; + @attr('string') lastName; + @attr('date') birthday; + } + ``` + + ```javascript + import Person from 'app/models/person' + + let attributes = Person.attributes + + attributes.forEach(function(meta, name) { + // do thing + }); + + // prints: + // firstName {type: "string", kind: 'attribute', options: Object, parentType: function, name: "firstName"} + // lastName {type: "string", kind: 'attribute', options: Object, parentType: function, name: "lastName"} + // birthday {type: "date", kind: 'attribute', options: Object, parentType: function, name: "birthday"} + ``` + + @property attributes + @public + @static + @type {Map} + @readOnly + */ + @computeOnce + static get attributes(): Map { + 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) => { + if (isAttributeSchema(meta)) { + assert( + "You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: attr('')` from " + + this.toString(), + name !== 'id' + ); + + // TODO deprecate key being here + (meta as unknown as { key: string }).key = name; + meta.name = name; + map.set(name, meta); + } + }); + + return map; + } + + /** + A map whose keys are the attributes of the model (properties + described by attr) and whose values are type of transformation + applied to each attribute. This map does not include any + attributes that do not have an transformation type. + + Example + + ```app/models/person.js + import Model, { attr } from '@ember-data/model'; + + export default class PersonModel extends Model { + @attr firstName; + @attr('string') lastName; + @attr('date') birthday; + } + ``` + + ```javascript + import Person from 'app/models/person'; + + let transformedAttributes = Person.transformedAttributes + + transformedAttributes.forEach(function(field, type) { + // do thing + }); + + // prints: + // lastName string + // birthday date + ``` + + @property transformedAttributes + @public + @static + @type {Map} + @readOnly + */ + @computeOnce + static get transformedAttributes() { + 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.eachAttribute((name: string, meta: LegacyAttributeField) => { + if (meta.type) { + map.set(name, meta.type); + } + }); + + return map; + } + + /** + Iterates through the attributes of the model, calling the passed function on each + attribute. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(name, meta); + ``` + + - `name` the name of the current property in the iteration + - `meta` the meta object for the attribute property in the iteration + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```javascript + import Model, { attr } from '@ember-data/model'; + + class PersonModel extends Model { + @attr('string') firstName; + @attr('string') lastName; + @attr('date') birthday; + } + + PersonModel.eachAttribute(function(name, meta) { + // do thing + }); + + // prints: + // firstName {type: "string", kind: 'attribute', options: Object, parentType: function, name: "firstName"} + // lastName {type: "string", kind: 'attribute', options: Object, parentType: function, name: "lastName"} + // birthday {type: "date", kind: 'attribute', options: Object, parentType: function, name: "birthday"} + ``` + + @method eachAttribute + @public + @param {Function} callback The callback to execute + @param {Object} [binding] the value to which the callback's `this` should be bound + @static + */ + static eachAttribute( + callback: (this: T | undefined, key: MaybeAttrFields, attribute: LegacyAttributeField) => void, + binding?: T + ): void { + 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); + }); + } + + /** + Iterates through the transformedAttributes of the model, calling + the passed function on each attribute. Note the callback will not be + called for any attributes that do not have an transformation type. + + The callback method you provide should have the following signature (all + parameters are optional): + + ```javascript + function(name, type); + ``` + + - `name` the name of the current property in the iteration + - `type` a string containing the name of the type of transformed + applied to the attribute + + Note that in addition to a callback, you can also pass an optional target + object that will be set as `this` on the context. + + Example + + ```javascript + import Model, { attr } from '@ember-data/model'; + + let Person = Model.extend({ + firstName: attr(), + lastName: attr('string'), + birthday: attr('date') + }); + + Person.eachTransformedAttribute(function(name, type) { + // do thing + }); + + // prints: + // lastName string + // birthday date + ``` + + @method eachTransformedAttribute + @public + @param {Function} callback The callback to execute + @param {Object} [binding] the value to which the callback's `this` should be bound + @static + */ + static eachTransformedAttribute( + callback: (this: T | undefined, key: Exclude, type: string) => void, + binding?: T + ): void { + 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); + }); + } + + /** + Returns the name of the model class. + + @method toString + @public + @static + */ + static toString() { + 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}`; + } +} + +// @ts-expect-error TS doesn't know how to do `this` function overloads +Model.prototype.save = save; +// @ts-expect-error TS doesn't know how to do `this` function overloads +Model.prototype.destroyRecord = destroyRecord; +Model.prototype.unloadRecord = unloadRecord; +Model.prototype.hasMany = hasMany; +Model.prototype.belongsTo = belongsTo; +Model.prototype.serialize = serialize; +Model.prototype._createSnapshot = createSnapshot; +Model.prototype.deleteRecord = deleteRecord; +Model.prototype.changedAttributes = changedAttributes; +Model.prototype.rollbackAttributes = rollbackAttributes; +Model.prototype.reload = reload; + +defineSignal(Model.prototype, 'isReloading', false); + +// this is required to prevent `init` from passing +// the values initialized during create to `setUnknownProperty` +(Model.prototype as unknown as { _createProps: null })._createProps = null; +(Model.prototype as unknown as { _secretInit: null })._secretInit = null; + +if (DEBUG) { + const lookupDescriptor = function lookupDescriptor(obj: object, keyName: string) { + let current: object = obj; + do { + const descriptor = Object.getOwnPropertyDescriptor(current, keyName); + if (descriptor !== undefined) { + return descriptor; + } + current = Object.getPrototypeOf(current) as object; + } while (current !== null); + return null; + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method + const init = Model.prototype.init; + Model.prototype.init = function (createArgs: ModelCreateArgs) { + init.call(this, createArgs); + + const ourDescriptor = lookupDescriptor(Model.prototype, 'currentState'); + const theirDescriptor = lookupDescriptor(this, 'currentState'); + + if (!ourDescriptor || !theirDescriptor) { + throw new Error( + `Unable to determine if 'currentState' is a reserved property name on instances of classes extending Model. Please ensure that 'currentState' is not defined as a property on ${this.constructor.toString()}` + ); + } + + const realState = this.___recordState; + if (ourDescriptor.get !== theirDescriptor.get || realState !== this.currentState) { + throw new Error( + `'currentState' is a reserved property name on instances of classes extending Model. Please choose a different property name for ${this.constructor.toString()}` + ); + } + + const ID_DESCRIPTOR = lookupDescriptor(Model.prototype, 'id'); + const idDesc = lookupDescriptor(this, 'id'); + + if (!ID_DESCRIPTOR || !idDesc) { + throw new Error( + `Unable to determine if 'id' is a reserved property name on instances of classes extending Model. Please ensure that 'id' is not defined as a property on ${this.constructor.toString()}` + ); + } + + if (idDesc.get !== ID_DESCRIPTOR.get) { + throw new Error( + `You may not set 'id' as an attribute on your model. Please remove any lines that look like: \`id: attr('')\` from ${this.constructor.toString()}` + ); + } + }; + + 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 }; + +function isRelationshipSchema(meta: unknown): meta is LegacyRelationshipSchema { + const hasKind = typeof meta === 'object' && meta !== null && 'kind' in meta && 'options' in meta; + return hasKind && (meta.kind === 'hasMany' || meta.kind === 'belongsTo'); +} + +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/model.type-test.ts b/packages/model/src/-private/model.type-test.ts new file mode 100644 index 00000000000..cd2f2a9c09b --- /dev/null +++ b/packages/model/src/-private/model.type-test.ts @@ -0,0 +1,306 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { expectTypeOf } from 'expect-type'; + +import Store from '@ember-data/store'; +import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +import { attr } from './attr'; +import { belongsTo } from './belongs-to'; +import { hasMany } from './has-many'; +import type { RelatedCollection as ManyArray } from './many-array'; +import { Model } from './model'; +import type { PromiseBelongsTo } from './promise-belongs-to'; +import type { PromiseManyArray } from './promise-many-array'; +import type BelongsToReference from './references/belongs-to'; +import type HasManyReference from './references/has-many'; +import type { isSubClass, MaybeAttrFields, MaybeBelongsToFields } from './type-utils'; + +// ------------------------------ +// 💚 +// ============================== +// Type Tests +// ============================== +// 🐹 +// ⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇ + +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); + +class UnbrandedUser extends Model { + @attr('string') declare name: string | null; + @hasMany('user', { async: false, inverse: null }) declare enemies: ManyArray; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: UnbrandedUser | null; + @hasMany('user', { async: true, inverse: 'friends' }) declare friends: PromiseManyArray; + @belongsTo('user', { async: true, inverse: 'twin' }) declare twin: PromiseBelongsTo; +} +const user = new UnbrandedUser(); + +expectTypeOf>().toEqualTypeOf<'name' | 'bestFriend'>(); +expectTypeOf>().toEqualTypeOf(); + +type DoesExtend = UnbrandedUser extends Model ? true : false; +function takeModel(model: T): T { + return model; +} + +expectTypeOf(takeModel(new UnbrandedUser())).toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); + +expectTypeOf>['modelName']>().toEqualTypeOf(); +expectTypeOf['modelName']>().toEqualTypeOf(); +expectTypeOf>().toMatchTypeOf(); + +expectTypeOf(user.name).toEqualTypeOf(); +expectTypeOf(user.enemies).toEqualTypeOf>(); +expectTypeOf(user.bestFriend).toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf>(); +expectTypeOf>().toEqualTypeOf(); + +class BrandedUser extends Model { + @attr('string') declare name: string | null; + @hasMany('user', { async: false, inverse: null }) declare enemies: ManyArray; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: BrandedUser | null; + @hasMany('user', { async: true, inverse: 'friends' }) declare friends: PromiseManyArray; + @belongsTo('user', { async: true, inverse: 'twin' }) declare twin: PromiseBelongsTo; + + [Type] = 'user' as const; +} +const branded = new BrandedUser(); + +expectTypeOf>().toEqualTypeOf<'name'>(); +expectTypeOf>().toEqualTypeOf(); + +expectTypeOf>['modelName']>().toEqualTypeOf<'user'>(); +expectTypeOf['modelName']>().toEqualTypeOf<'user'>(); +expectTypeOf>().toMatchTypeOf(); + +expectTypeOf(branded.name).toEqualTypeOf(); +expectTypeOf(branded.enemies).toEqualTypeOf>(); +expectTypeOf(branded.bestFriend).toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf>(); +expectTypeOf>().toEqualTypeOf(); + +class BrandedTypedUser extends Model { + @attr('string') declare name: string | null; + @hasMany('user', { async: false, inverse: null }) declare enemies: ManyArray; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: BrandedTypedUser; + @hasMany('user', { async: true, inverse: 'friends' }) + declare friends: PromiseManyArray; + @belongsTo('user', { async: true, inverse: 'twin' }) + declare twin: PromiseBelongsTo; + @hasMany('user', { async: false, inverse: 'leader' }) + declare crew: PromiseManyArray; + @belongsTo('user', { async: false, inverse: 'crew' }) + declare leader: PromiseBelongsTo; + + [Type] = 'user' as const; +} +const brandedAndTyped = new BrandedTypedUser(); + +expectTypeOf>['modelName']>().toEqualTypeOf<'user'>(); +expectTypeOf['modelName']>().toEqualTypeOf<'user'>(); +expectTypeOf>().toMatchTypeOf(); + +expectTypeOf(brandedAndTyped.name).toEqualTypeOf(); +expectTypeOf(brandedAndTyped.enemies).toEqualTypeOf>(); +expectTypeOf(brandedAndTyped.bestFriend).toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf>(); +expectTypeOf>().toEqualTypeOf(); + +// ------------------------------ +// References +// ------------------------------ + +expectTypeOf( + user.belongsTo( + // @ts-expect-error + 'bestFriends' + ) +).toBeNever; + +// bestFriend is a never because +// the value it points to is not branded +// we could make it *mostly* work but that would +// make other types less useful. +expectTypeOf( + user.belongsTo( + // @ts-expect-error + 'bestFriend' + ) +).toBeNever; + +// const bestFriend = user.belongsTo('bestFriend'); +// expectTypeOf(bestFriend).toEqualTypeOf>(); +// expectTypeOf(bestFriend.___identifier.type).toEqualTypeOf(); +// expectTypeOf(bestFriend.identifier!.type).toEqualTypeOf(); +// expectTypeOf(bestFriend.key).toEqualTypeOf<'bestFriend'>(); +// expectTypeOf(bestFriend.type).toEqualTypeOf(); +// expectTypeOf(bestFriend.value()).toEqualTypeOf(); + +expectTypeOf(user.belongsTo('twin')).toEqualTypeOf>(); +expectTypeOf(user.belongsTo('twin').___identifier.type).toEqualTypeOf(); +expectTypeOf(user.belongsTo('twin').identifier!.type).toEqualTypeOf(); +expectTypeOf(user.belongsTo('twin').key).toEqualTypeOf<'twin'>(); +expectTypeOf(user.belongsTo('twin').type).toEqualTypeOf(); +expectTypeOf(user.belongsTo('twin').value()).toEqualTypeOf(); + +expectTypeOf(branded.belongsTo('bestFriend')).toEqualTypeOf>(); +expectTypeOf(branded.belongsTo('bestFriend').___identifier.type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.belongsTo('bestFriend').identifier!.type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.belongsTo('bestFriend').___identifier.type).not.toEqualTypeOf(); +expectTypeOf(branded.belongsTo('bestFriend').identifier!.type).not.toEqualTypeOf(); +expectTypeOf(branded.belongsTo('bestFriend').key).toEqualTypeOf<'bestFriend'>(); +expectTypeOf(branded.belongsTo('bestFriend').type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.belongsTo('bestFriend').value()).toEqualTypeOf(); + +expectTypeOf( + user.hasMany( + // @ts-expect-error + 'bestFriends' + ) +).toBeNever; + +expectTypeOf(user.hasMany('enemies')).toEqualTypeOf>(); +expectTypeOf(user.hasMany('enemies').___identifier.type).toEqualTypeOf(); +expectTypeOf(user.hasMany('enemies').identifiers[0].type).toEqualTypeOf(); +expectTypeOf(user.hasMany('enemies').key).toEqualTypeOf<'enemies'>(); +expectTypeOf(user.hasMany('enemies').type).toEqualTypeOf(); +expectTypeOf(user.hasMany('enemies').value()).toMatchTypeOf(); + +expectTypeOf(branded.hasMany('enemies')).toEqualTypeOf>(); +expectTypeOf(branded.hasMany('enemies').___identifier.type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.hasMany('enemies').identifiers[0].type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.hasMany('enemies').___identifier.type).not.toEqualTypeOf(); +expectTypeOf(branded.hasMany('enemies').identifiers[0].type).not.toEqualTypeOf(); +expectTypeOf(branded.hasMany('enemies').key).toEqualTypeOf<'enemies'>(); +expectTypeOf(branded.hasMany('enemies').type).toEqualTypeOf<'user'>(); +expectTypeOf(branded.hasMany('enemies').value()).toMatchTypeOf(); + +// these ensure subclasses satisfy Model +function takesAModel(arg: Model) {} +takesAModel(user); +takesAModel(branded); + +user.eachAttribute((key, meta) => { + // bestFriend is in the wrong place because the records aren't branded + expectTypeOf(key).toEqualTypeOf<'name' | 'bestFriend'>(); + expectTypeOf(meta).toEqualTypeOf(); +}); +user.eachRelationship((key, meta) => { + expectTypeOf(key).toEqualTypeOf<'twin' | 'enemies' | 'friends'>(); + expectTypeOf(meta).toEqualTypeOf(); +}); + +branded.eachAttribute((key, meta) => { + expectTypeOf(key).toEqualTypeOf<'name'>(); + expectTypeOf(meta).toEqualTypeOf(); +}); + +branded.eachRelationship((key, meta) => { + expectTypeOf(key).toEqualTypeOf<'bestFriend' | 'twin' | 'enemies' | 'friends'>(); + expectTypeOf(meta).toEqualTypeOf(); +}); + +// this ensures that `serialize` can be overridden +class UserWithCustomSerialize extends Model { + @attr('string') declare name: string | null; + + serialize() { + return { name: this.name }; + } +} +expectTypeOf(new UserWithCustomSerialize().serialize()).toEqualTypeOf<{ name: string | null }>(); +class FooModel extends Model { + [Type] = 'foo' as const; + + private myMethod() { + // ... + } + + save(options?: Record): Promise { + if (this.currentState.isNew && this.currentState.isDeleted) { + return Promise.resolve(this); + } + + this.myMethod(); + + return super.save(options); + } +} + +expectTypeOf(new FooModel().save()).toEqualTypeOf>(); + +const store = new Store(); + +type CreateProps = Parameters>[1]; + +expectTypeOf({ + name: 'foo', + bestFriend: null, + enemies: [], + friends: [], + twin: null, +}).toMatchTypeOf(); + +expectTypeOf({ notAProp: 'nope' }).not.toMatchTypeOf(); +expectTypeOf({ crew: [] }).not.toMatchTypeOf(); + +store.createRecord('user', { + name: 'foo', + bestFriend: null, + enemies: [], + friends: [], + twin: null, + // @ts-expect-error not a field + crew: [], +}); + +store.createRecord('user', { + name: 'foo', + bestFriend: null, + enemies: [], + friends: [], + twin: null, + // @ts-expect-error not a field + notAField: 'nope', +}); + +store.createRecord('user', { + name: 'foo', + bestFriend: null, + enemies: [], + friends: [], + twin: null, + // @ts-expect-error is a Model field + isNew: true, +}); + +store.createRecord('user', { + name: 'foo', + bestFriend: null, + enemies: [], + friends: [], + twin: null, + // @ts-expect-error is an EmberObject field + isDestroyed: true, +}); + +class HasGetter extends Model { + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: BrandedUser | null; + + get bestFriendId(): string | null { + return this.belongsTo('bestFriend').id(); + } +} +const hasGetter = new HasGetter(); +expectTypeOf>().toEqualTypeOf<'bestFriend'>(); +expectTypeOf(hasGetter.belongsTo('bestFriend').id()).toEqualTypeOf(); + +function expectsArray(array: T[]) {} + +// ManyArray works +expectsArray(branded.enemies); + +// PromiseManyArray works only if awaited +expectsArray(await branded.friends); diff --git a/packages/model/src/-private/notify-changes.ts b/packages/model/src/-private/notify-changes.ts index 83a6939ee71..c97c80b4644 100644 --- a/packages/model/src/-private/notify-changes.ts +++ b/packages/model/src/-private/notify-changes.ts @@ -1,13 +1,13 @@ import { cacheFor } from '@ember/object/internals'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; import type Store from '@ember-data/store'; -import { peekCache } from '@ember-data/store/-private'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { NotificationType } from '@ember-data/store'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; -import type Model from './model'; -import { LEGACY_SUPPORT } from './model'; +import { LEGACY_SUPPORT } from './legacy-relationships-support'; +import type { Model } from './model'; export default function notifyChanges( identifier: StableRecordIdentifier, @@ -20,17 +20,18 @@ export default function notifyChanges( if (key) { notifyAttribute(store, identifier, key, record); } else { - record.eachAttribute((key) => { - notifyAttribute(store, identifier, key, record); + record.eachAttribute((name) => { + notifyAttribute(store, identifier, name, record); }); } } else if (value === 'relationships') { if (key) { - let meta = record.constructor.relationshipsByName.get(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((key, meta) => { - notifyRelationship(identifier, key, record, meta); + record.eachRelationship((name, meta) => { + notifyRelationship(identifier, name, record, meta); }); } } else if (value === 'identity') { @@ -38,13 +39,13 @@ export default function notifyChanges( } } -function notifyRelationship(identifier: StableRecordIdentifier, key: string, record: Model, meta) { +function notifyRelationship(identifier: StableRecordIdentifier, key: string, record: Model, meta: RelationshipSchema) { if (meta.kind === 'belongsTo') { record.notifyPropertyChange(key); } else if (meta.kind === 'hasMany') { - let support = LEGACY_SUPPORT.get(identifier); - let manyArray = support && support._manyArrayCache[key]; - let hasPromise = support && support._relationshipPromisesCache[key]; + const support = LEGACY_SUPPORT.get(identifier); + const manyArray = support && support._manyArrayCache[key]; + const hasPromise = support && support._relationshipPromisesCache[key]; if (manyArray && hasPromise) { // do nothing, we will notify the ManyArray directly @@ -58,7 +59,9 @@ function notifyRelationship(identifier: StableRecordIdentifier, key: string, rec //We need to notifyPropertyChange in the adding case because we need to make sure //we fetch the newly added record in case it is unloaded //TODO(Igor): Consider whether we could do this only if the record state is unloaded - if (!meta.options || meta.options.async || meta.options.async === undefined) { + assert(`Expected options to exist on relationship meta`, meta.options); + assert(`Expected async to exist on relationship meta options`, 'async' in meta.options); + if (meta.options.async) { record.notifyPropertyChange(key); } } @@ -66,8 +69,8 @@ function notifyRelationship(identifier: StableRecordIdentifier, key: string, rec } function notifyAttribute(store: Store, identifier: StableRecordIdentifier, key: string, record: Model) { - let currentValue = cacheFor(record, key); - const cache = DEPRECATE_V1_RECORD_DATA ? peekCache(record)! : store.cache; + const currentValue = cacheFor(record, key); + const cache = store.cache; if (currentValue !== cache.getAttr(identifier, key)) { record.notifyPropertyChange(key); } diff --git a/packages/model/src/-private/promise-belongs-to.ts b/packages/model/src/-private/promise-belongs-to.ts index 0988c3547f0..1abca141273 100644 --- a/packages/model/src/-private/promise-belongs-to.ts +++ b/packages/model/src/-private/promise-belongs-to.ts @@ -1,36 +1,38 @@ -import { assert } from '@ember/debug'; import { computed } from '@ember/object'; import type PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import type ObjectProxy from '@ember/object/proxy'; -import { cached } from '@glimmer/tracking'; import type Store from '@ember-data/store'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { Dict } from '@ember-data/types/q/utils'; +import { cached } from '@ember-data/tracking'; +import { assert } from '@warp-drive/build-config/macros'; +import type { OpaqueRecordInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; -import { LegacySupport } from './legacy-relationships-support'; +import type { LegacySupport } from './legacy-relationships-support'; import { PromiseObject } from './promise-proxy-base'; -import type BelongsToReference from './references/belongs-to'; -export interface BelongsToProxyMeta { +export interface BelongsToProxyMeta { key: string; store: Store; legacySupport: LegacySupport; - modelName: string; + modelName: TypeFromInstanceOrString; } -export interface BelongsToProxyCreateArgs { - promise: Promise; - content?: RecordInstance | null; - _belongsToState: BelongsToProxyMeta; +export interface BelongsToProxyCreateArgs { + promise: Promise; + content?: T | null; + _belongsToState: BelongsToProxyMeta; } -interface PromiseObjectType extends PromiseProxyMixin, ObjectProxy { - new (...args: unknown[]): PromiseObjectType; +export const LegacyPromiseProxy = Symbol.for('LegacyPromiseProxy'); + +interface PromiseObjectType extends PromiseProxyMixin, ObjectProxy { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (...args: unknown[]): PromiseObjectType; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -declare class PromiseObjectType {} +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-extraneous-class +declare class PromiseObjectType {} -const Extended: PromiseObjectType = PromiseObject as unknown as PromiseObjectType; +const Extended: PromiseObjectType = + PromiseObject as unknown as PromiseObjectType; /** @module @ember-data/model @@ -45,13 +47,13 @@ const Extended: PromiseObjectType = PromiseObject as unknown as @extends PromiseObject @private */ -class PromiseBelongsTo extends Extended { - declare _belongsToState: BelongsToProxyMeta; +class PromiseBelongsTo extends Extended { + declare _belongsToState: BelongsToProxyMeta; @cached - get id() { + get id(): string | null { const { key, legacySupport } = this._belongsToState; - const ref = legacySupport.referenceFor('belongsTo', key) as BelongsToReference; + const ref = legacySupport.referenceFor('belongsTo', key); return ref.id(); } @@ -65,7 +67,7 @@ class PromiseBelongsTo extends Extended { if (1) { assert( 'You attempted to access meta on the promise for the async belongsTo relationship ' + - `${this.get('_belongsToState').modelName}:${this.get('_belongsToState').key}'.` + + `${this._belongsToState.modelName}:${this._belongsToState.key}'.` + '\nUse `record.belongsTo(relationshipName).meta()` instead.', false ); @@ -73,12 +75,14 @@ class PromiseBelongsTo extends Extended { return; } - async reload(options: Dict): Promise { + async reload(options: Record): Promise { assert('You are trying to reload an async belongsTo before it has been created', this.content !== undefined); - let { key, legacySupport } = this._belongsToState; + const { key, legacySupport } = this._belongsToState; await legacySupport.reloadBelongsTo(key, options); return this; } + + [LegacyPromiseProxy] = true as const; } -export default PromiseBelongsTo; +export { PromiseBelongsTo }; diff --git a/packages/model/src/-private/promise-many-array.ts b/packages/model/src/-private/promise-many-array.ts index fcd7ecbe0e1..739195c1801 100644 --- a/packages/model/src/-private/promise-many-array.ts +++ b/packages/model/src/-private/promise-many-array.ts @@ -1,25 +1,26 @@ import ArrayMixin, { NativeArray } from '@ember/array'; import type ArrayProxy from '@ember/array/proxy'; -import { assert, deprecate } from '@ember/debug'; -import { dependentKeyCompat } from '@ember/object/compat'; -import { tracked } from '@glimmer/tracking'; +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_A_USAGE, DEPRECATE_COMPUTED_CHAINS, DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS, -} from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import { FindOptions } from '@ember-data/types/q/store'; +} from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; -import type ManyArray from './many-array'; +import type { RelatedCollection as ManyArray } from './many-array'; +import { LegacyPromiseProxy } from './promise-belongs-to'; -export interface HasManyProxyCreateArgs { - promise: Promise; - content?: ManyArray; +export interface HasManyProxyCreateArgs { + promise: Promise>; + content?: ManyArray; } /** @@ -41,31 +42,31 @@ export interface HasManyProxyCreateArgs { @class PromiseManyArray @public */ -export default interface PromiseManyArray extends Omit, 'destroy'> { - createRecord(): RecordInstance; - reload(options: FindOptions): PromiseManyArray; +export interface PromiseManyArray extends Omit, 'destroy' | 'forEach'> { + createRecord(hash: CreateRecordProperties): T; + reload(options: Omit): PromiseManyArray; } -export default class PromiseManyArray { - declare promise: Promise | null; +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) { + 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) => { + 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', }); - // @ts-expect-error ArrayMixin is more than a type if (mixin === NativeArray || mixin === ArrayMixin) { return true; } @@ -73,7 +74,7 @@ export default class PromiseManyArray { }; } else if (DEBUG) { const meta = Ember.meta(this); - meta.hasMixin = (mixin: Object) => { + meta.hasMixin = (mixin: object) => { assert(`Do not use A() on an EmberData PromiseManyArray`); }; } @@ -81,34 +82,22 @@ export default class PromiseManyArray { //---- Methods/Properties on ArrayProxy that we will keep as our API - @tracked content: any | null = null; - /** * Retrieve the length of the content * @property length * @public */ - @dependentKeyCompat + @compat get length(): number { // shouldn't be needed, but ends up being needed // for computed chains even in 4.x if (DEPRECATE_COMPUTED_CHAINS) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this['[]']; } return this.content ? this.content.length : 0; } - // ember-source < 3.23 (e.g. 3.20 lts) - // requires that the tag `'[]'` be notified - // on the ArrayProxy in order for `{{#each}}` - // to recompute. We entangle the '[]' tag from - @dependentKeyCompat - get '[]'() { - if (DEPRECATE_COMPUTED_CHAINS) { - return this.content?.length && this.content; - } - } - /** * Iterate the proxied content. Called by the glimmer iterator in #each * We do not guarantee that forEach will always be available. This @@ -119,7 +108,7 @@ export default class PromiseManyArray { * @returns * @private */ - forEach(cb) { + forEach(cb: (item: T, index: number, array: T[]) => void) { if (this.content && this.length) { this.content.forEach(cb); } @@ -132,9 +121,9 @@ export default class PromiseManyArray { * @param options * @returns */ - reload(options: FindOptions) { + reload(options: Omit) { assert('You are trying to reload an async manyArray before it has been created', this.content); - this.content.reload(options); + void this.content.reload(options); return this; } @@ -146,28 +135,28 @@ export default class PromiseManyArray { * @property {boolean} isPending * @public */ - @tracked isPending: boolean = false; + declare isPending: boolean; /** * Whether the loading promise rejected * * @property {boolean} isRejected * @public */ - @tracked isRejected: boolean = false; + declare isRejected: boolean; /** * Whether the loading promise succeeded * * @property {boolean} isFulfilled * @public */ - @tracked isFulfilled: boolean = false; + declare isFulfilled: boolean; /** * Whether the loading promise completed (resolved or rejected) * * @property {boolean} isSettled * @public */ - @tracked isSettled: boolean = false; + declare isSettled: boolean; /** * chain this promise @@ -178,7 +167,7 @@ export default class PromiseManyArray { * @param fail * @returns Promise */ - then(s, f) { + then(s: Parameters>['then']>[0], f?: Parameters>['then']>[1]) { return this.promise!.then(s, f); } @@ -189,7 +178,7 @@ export default class PromiseManyArray { * @param callback * @returns Promise */ - catch(cb) { + catch(cb: Parameters>['catch']>[0]) { return this.promise!.catch(cb); } @@ -201,7 +190,7 @@ export default class PromiseManyArray { * @param callback * @returns Promise */ - finally(cb) { + finally(cb: Parameters>['finally']>[0]) { return this.promise!.finally(cb); } @@ -221,7 +210,7 @@ export default class PromiseManyArray { * @property links * @public */ - @dependentKeyCompat + @compat get links() { return this.content ? this.content.links : undefined; } @@ -231,14 +220,14 @@ export default class PromiseManyArray { * @property meta * @public */ - @dependentKeyCompat + @compat get meta() { return this.content ? this.content.meta : undefined; } //---- Our own stuff - _update(promise: Promise, content?: ManyArray) { + _update(promise: Promise>, content?: ManyArray) { if (content !== undefined) { this.content = content; } @@ -246,13 +235,45 @@ export default class PromiseManyArray { this.promise = tapPromise(this, promise); } - static create({ promise, content }: HasManyProxyCreateArgs): PromiseManyArray { + static create({ promise, content }: HasManyProxyCreateArgs): PromiseManyArray { return new this(promise, content); } + + [LegacyPromiseProxy] = true as const; +} +defineSignal(PromiseManyArray.prototype, 'content', null); +defineSignal(PromiseManyArray.prototype, 'isPending', false); +defineSignal(PromiseManyArray.prototype, 'isRejected', false); +defineSignal(PromiseManyArray.prototype, 'isFulfilled', false); +defineSignal(PromiseManyArray.prototype, 'isSettled', false); + +// this will error if someone tries to call +// A(identifierArray) since it is not configurable +// which is preferrable to the `meta` override we used +// before which required importing all of Ember +if (DEPRECATE_COMPUTED_CHAINS) { + const desc = { + enumerable: true, + configurable: false, + get: function (this: PromiseManyArray) { + return this.content?.length && this.content; + }, + }; + compat(PromiseManyArray.prototype, '[]', desc); + + // ember-source < 3.23 (e.g. 3.20 lts) + // requires that the tag `'[]'` be notified + // on the ArrayProxy in order for `{{#each}}` + // to recompute. We entangle the '[]' tag from content + + Object.defineProperty(PromiseManyArray.prototype, '[]', desc); } if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { - PromiseManyArray.prototype.createRecord = function createRecord(...args) { + 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, @@ -264,7 +285,7 @@ if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { } ); assert('You are trying to createRecord on an async manyArray before it has been created', this.content); - return this.content.createRecord(...args); + return this.content.createRecord(hash); }; Object.defineProperty(PromiseManyArray.prototype, 'firstObject', { @@ -279,6 +300,8 @@ if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { 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; }, }); @@ -295,12 +318,14 @@ if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { 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) { +function tapPromise(proxy: PromiseManyArray, promise: Promise>) { proxy.isPending = true; proxy.isSettled = false; proxy.isFulfilled = false; @@ -349,6 +374,8 @@ if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { for: 'ember-data', } ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return Ember[method](this, ...args); }; }); @@ -417,6 +444,7 @@ if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { } ); 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/record-state.ts b/packages/model/src/-private/record-state.ts index 805b2fad1f3..1bd944d1a01 100644 --- a/packages/model/src/-private/record-state.ts +++ b/packages/model/src/-private/record-state.ts @@ -1,119 +1,73 @@ -import { assert } from '@ember/debug'; -import { dependentKeyCompat } from '@ember/object/compat'; -import { cached, tracked } from '@glimmer/tracking'; - -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; import type Store from '@ember-data/store'; +import type { NotificationType } from '@ember-data/store'; import { storeFor } from '@ember-data/store'; -import { peekCache, recordIdentifierFor } from '@ember-data/store/-private'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type RequestStateService from '@ember-data/store/-private/network/request-cache'; -import { addToTransaction, subscribe } from '@ember-data/tracking/-private'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RequestState, RequestStateService } from '@ember-data/store/-private'; +import { recordIdentifierFor } from '@ember-data/store/-private'; +import { cached, compat } from '@ember-data/tracking'; +import { addToTransaction, defineSignal, getSignal, peekSignal, subscribe } from '@ember-data/tracking/-private'; +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'; -import Model from './model'; +import type { Errors } from './errors'; +import type { MinimalLegacyRecord } from './model-methods'; const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/; const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/; const PRIMARY_ATTRIBUTE_KEY = 'base'; -function isInvalidError(error) { - return error && error.isAdapterError === true && error.code === 'InvalidError'; -} - -/** - * Tag provides a cache for a getter - * that recomputes only when a specific - * tracked property that it manages is dirtied. - * - * This allows us to bust the cache for a value - * that otherwise doesn't access anything tracked - * as well as control the timing of that notification. - * - * @internal - */ -class Tag { - declare rev: number; - declare isDirty: boolean; - declare value: any; - declare t: boolean; - declare _debug_base: string; - declare _debug_prop: string; - - constructor() { - if (DEBUG) { - const [base, prop] = arguments as unknown as [string, string]; - this._debug_base = base; - this._debug_prop = prop; - } - this.rev = 1; - this.isDirty = true; - this.value = undefined; - /* - * whether this was part of a transaction when mutated - */ - this.t = false; - } - @tracked ref = null; - - notify() { - this.isDirty = true; - addToTransaction(this); - this.rev++; - } - consume(v) { - this.isDirty = false; - this.value = v; // set cached value - } -} - -const Tags = new WeakMap(); -function getTag(record, key) { - let tags = Tags.get(record); - if (!tags) { - tags = Object.create(null); - Tags.set(record, tags); - } - // @ts-expect-error - return (tags[key] = tags[key] || (DEBUG ? new Tag(record.constructor.modelName, key) : new Tag())); -} - -export function peekTag(record, key) { - let tags = Tags.get(record); - return tags && tags[key]; +function isInvalidError(error: unknown): error is Error & { isAdapterError: true; code: 'InvalidError' } { + return ( + !!error && + error instanceof Error && + 'isAdapterError' in error && + error.isAdapterError === true && + 'code' in error && + error.code === 'InvalidError' + ); } /** - * A decorattor that caches a getter while + * A decorator that caches a getter while * providing the ability to bust that cache * when we so choose in a way that notifies - * glimmer's tracking system. + * tracking systems. * * @internal */ -export function tagged(_target, key, desc) { - const getter = desc.get; - const setter = desc.set; - desc.get = function () { - let tag = getTag(this, key); - subscribe(tag); - - if (tag.isDirty) { - tag.consume(getter.call(this)); +export function tagged(_target: T, key: K, desc: PropertyDescriptor) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const getter = desc.get as (this: T) => unknown; + // eslint-disable-next-line @typescript-eslint/unbound-method + const setter = desc.set as (this: T, v: unknown) => void; + + desc.get = function (this: T) { + const signal = getSignal(this, key, true); + subscribe(signal); + + if (signal.shouldReset) { + signal.shouldReset = false; + signal.lastValue = getter.call(this); } - return tag.value; + return signal.lastValue; }; - desc.set = function (v) { - getTag(this, key); // ensure tag is setup in case we want to use it. + desc.set = function (this: T, v: unknown) { + getSignal(this, key, true); // ensure signal is setup in case we want to use it. // probably notify here but not yet. setter.call(this, v); }; - dependentKeyCompat(desc); + compat(desc); return desc; } +export function notifySignal(obj: T, key: K) { + const signal = peekSignal(obj, key); + if (signal) { + signal.shouldReset = true; + addToTransaction(signal); + } +} + /** Historically EmberData managed a state machine for each record, the localState for which @@ -156,24 +110,24 @@ root export default class RecordState { declare store: Store; declare identifier: StableRecordIdentifier; - declare record: Model; + declare record: MinimalLegacyRecord; declare rs: RequestStateService; declare pendingCount: number; declare fulfilledCount: number; declare rejectedCount: number; declare cache: Cache; - declare _errorRequests: any[]; - declare _lastError: any; + declare _errorRequests: RequestState[]; + declare _lastError: RequestState | null; declare handler: object; - constructor(record: Model) { + constructor(record: MinimalLegacyRecord) { const store = storeFor(record)!; const identity = recordIdentifierFor(record); this.identifier = identity; this.record = record; - this.cache = DEPRECATE_V1_RECORD_DATA ? peekCache(record)! : store.cache; + this.cache = store.cache; this.pendingCount = 0; this.fulfilledCount = 0; @@ -181,10 +135,10 @@ export default class RecordState { this._errorRequests = []; this._lastError = null; - let requests = store.getRequestStateService(); - let notifications = store.notifications; + const requests = store.getRequestStateService(); + const notifications = store.notifications; - const handleRequest = (req) => { + const handleRequest = (req: RequestState) => { if (req.type === 'mutation') { switch (req.state) { case 'pending': @@ -203,6 +157,7 @@ export default class RecordState { this._errorRequests = []; this._lastError = null; this.isSaving = false; + this.notify('isDirty'); notifyErrorsStateChanged(this); break; } @@ -238,11 +193,9 @@ export default class RecordState { // we instantiate lazily // so we grab anything we don't have yet - if (!DEBUG) { - const lastRequest = requests.getLastRequestForRecord(identity); - if (lastRequest) { - handleRequest(lastRequest); - } + const lastRequest = requests.getLastRequestForRecord(identity); + if (lastRequest) { + handleRequest(lastRequest); } this.handler = notifications.subscribe( @@ -250,6 +203,7 @@ export default class RecordState { (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => { switch (type) { case 'state': + this.notify('isSaved'); this.notify('isNew'); this.notify('isDeleted'); this.notify('isDirty'); @@ -271,24 +225,24 @@ export default class RecordState { storeFor(this.record)!.notifications.unsubscribe(this.handler); } - notify(key) { - getTag(this, key).notify(); + notify(key: keyof this & string) { + notifySignal(this, key); } - updateInvalidErrors(errors) { + updateInvalidErrors(errors: Errors) { assert( - `Expected the Cache instance for ${this.identifier} to implement getErrors(identifier)`, + `Expected the Cache instance for ${this.identifier.lid} to implement getErrors(identifier)`, typeof this.cache.getErrors === 'function' ); - let jsonApiErrors = this.cache.getErrors(this.identifier); + const jsonApiErrors = this.cache.getErrors(this.identifier); errors.clear(); for (let i = 0; i < jsonApiErrors.length; i++) { - let error = jsonApiErrors[i]; + const error = jsonApiErrors[i]; if (error.source && error.source.pointer) { - let keyMatch = error.source.pointer.match(SOURCE_POINTER_REGEXP); + const keyMatch = error.source.pointer.match(SOURCE_POINTER_REGEXP); let key: string | undefined; if (keyMatch) { @@ -298,7 +252,8 @@ export default class RecordState { } if (key) { - let errMsg = error.detail || error.title; + const errMsg = error.detail || error.title; + assert(`Expected field error to have a detail or title to use as the message`, errMsg); errors.add(key, errMsg); } } @@ -313,7 +268,7 @@ export default class RecordState { this._lastError = null; } - @tracked isSaving = false; + declare isSaving: boolean; @tagged get isLoading() { @@ -330,9 +285,9 @@ export default class RecordState { @tagged get isSaved() { - let rd = this.cache; + const rd = this.cache; if (this.isDeleted) { - assert(`Expected Cache to implement isDeletionCommitted()`, rd.isDeletionCommitted); + assert(`Expected Cache to implement isDeletionCommitted()`, typeof rd.isDeletionCommitted === 'function'); return rd.isDeletionCommitted(this.identifier); } if (this.isNew || this.isEmpty || !this.isValid || this.isDirty || this.isLoading) { @@ -343,24 +298,24 @@ export default class RecordState { @tagged get isEmpty() { - let rd = this.cache; + const rd = this.cache; // TODO this is not actually an RFC'd concept. Determine the // correct heuristic to replace this with. - assert(`Expected Cache to implement isEmpty()`, rd.isEmpty); + assert(`Expected Cache to implement isEmpty()`, typeof rd.isEmpty === 'function'); return !this.isNew && rd.isEmpty(this.identifier); } @tagged get isNew() { - let rd = this.cache; - assert(`Expected Cache to implement isNew()`, rd.isNew); + const rd = this.cache; + assert(`Expected Cache to implement isNew()`, typeof rd.isNew === 'function'); return rd.isNew(this.identifier); } @tagged get isDeleted() { - let rd = this.cache; - assert(`Expected Cache to implement isDeleted()`, rd.isDeleted); + const rd = this.cache; + assert(`Expected Cache to implement isDeleted()`, typeof rd.isDeleted === 'function'); return rd.isDeleted(this.identifier); } @@ -371,16 +326,16 @@ export default class RecordState { @tagged get isDirty() { - let rd = this.cache; - if (rd.isDeletionCommitted(this.identifier) || (this.isDeleted && this.isNew)) { + const rd = this.cache; + if (this.isEmpty || rd.isDeletionCommitted(this.identifier) || (this.isDeleted && this.isNew)) { return false; } - return this.isNew || rd.hasChangedAttrs(this.identifier); + return this.isDeleted || this.isNew || rd.hasChangedAttrs(this.identifier); } @tagged get isError() { - let errorReq = this._errorRequests[this._errorRequests.length - 1]; + const errorReq = this._errorRequests[this._errorRequests.length - 1]; if (!errorReq) { return false; } else { @@ -390,11 +345,11 @@ export default class RecordState { @tagged get adapterError() { - let request = this._lastError; + const request = this._lastError; if (!request) { return null; } - return request.state === 'rejected' && request.response.data; + return request.state === 'rejected' && request.response!.data; } @cached @@ -455,7 +410,7 @@ export default class RecordState { return ''; // deleted substates - } else if (this.isDeleted) { + } else if (this.isDirty && this.isDeleted) { return 'deleted'; // loaded.created substates @@ -472,6 +427,7 @@ export default class RecordState { } } } +defineSignal(RecordState.prototype, 'isSaving', false); function notifyErrorsStateChanged(state: RecordState) { state.notify('isValid'); diff --git a/packages/model/src/-private/references/belongs-to.ts b/packages/model/src/-private/references/belongs-to.ts index f4ebd033bd3..c3b227115e5 100644 --- a/packages/model/src/-private/references/belongs-to.ts +++ b/packages/model/src/-private/references/belongs-to.ts @@ -1,29 +1,29 @@ import { deprecate } from '@ember/debug'; -import { dependentKeyCompat } from '@ember/object/compat'; -import { cached, tracked } from '@glimmer/tracking'; -import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; - -import { DEPRECATE_PROMISE_PROXIES, DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import type { Graph } from '@ember-data/graph/-private/graph/graph'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; +import type { Graph, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import { recordIdentifierFor } from '@ember-data/store/-private'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; +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'; +import type { TypeFromInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { LinkObject, Links, + Meta, SingleResourceDocument, SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { Dict } from '@ember-data/types/q/utils'; +} from '@warp-drive/core-types/spec/json-api-raw'; +import type { IsUnknown } from '../belongs-to'; import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; -import { areAllInverseRecordsLoaded, LegacySupport } from '../legacy-relationships-support'; -import { LEGACY_SUPPORT } from '../model'; +import type { LegacySupport } from '../legacy-relationships-support'; +import { areAllInverseRecordsLoaded, LEGACY_SUPPORT } from '../legacy-relationships-support'; +import type { MaybeBelongsToFields } from '../type-utils'; +import { isMaybeResource } from './has-many'; /** @module @ember-data/model @@ -33,7 +33,7 @@ interface ResourceIdentifier { links?: { related?: string | LinkObject; }; - meta?: JSONObject; + meta?: Meta; } function isResourceIdentiferWithRelatedLinks( @@ -43,40 +43,78 @@ function isResourceIdentiferWithRelatedLinks( } /** - A `BelongsToReference` is a low-level API that allows users and - addon authors to perform meta-operations on a belongs-to - relationship. + A `BelongsToReference` is a low-level API that allows access + and manipulation of a belongsTo relationship. + + It is especially useful when you're dealing with `async` relationships + from `@ember-data/model` as it allows synchronous access to + the relationship data if loaded, as well as APIs for loading, reloading + the data or accessing available information without triggering a load. + + It may also be useful when using `sync` relationships with `@ember-data/model` + that need to be loaded/reloaded with more precise timing than marking the + relationship as `async` and relying on autofetch would have allowed. + + However,keep in mind that marking a relationship as `async: false` will introduce + bugs into your application if the data is not always guaranteed to be available + by the time the relationship is accessed. Ergo, it is recommended when using this + approach to utilize `links` for unloaded relationship state instead of identifiers. + + Reference APIs are entangled with the relationship's underlying state, + thus any getters or cached properties that utilize these will properly + invalidate if the relationship state changes. + + References are "stable", meaning that multiple calls to retrieve the reference + for a given relationship will always return the same HasManyReference. @class BelongsToReference @public */ -export default class BelongsToReference { - declare key: string; - declare belongsToRelationship: BelongsToRelationship; - declare type: string; - ___identifier: StableRecordIdentifier; - declare store: Store; +export default class BelongsToReference< + T = unknown, + K extends string = IsUnknown extends true ? string : MaybeBelongsToFields, + Related = K extends keyof T ? Exclude, null> : unknown, +> { declare graph: Graph; + declare store: Store; + declare belongsToRelationship: ResourceEdge; + /** + * The field name on the parent record for this has-many relationship. + * + * @property {String} key + * @public + */ + declare key: K; + + /** + * The type of resource this relationship will contain. + * + * @property {String} type + * @public + */ + declare type: TypeFromInstanceOrString; // unsubscribe tokens given to us by the notification manager - ___token!: object; - ___relatedToken: object | null = null; + declare ___token: object; + declare ___identifier: StableRecordIdentifier>; + declare ___relatedToken: object | null; - @tracked _ref = 0; + declare _ref: number; constructor( store: Store, graph: Graph, - parentIdentifier: StableRecordIdentifier, - belongsToRelationship: BelongsToRelationship, - key: string + parentIdentifier: StableRecordIdentifier>, + belongsToRelationship: ResourceEdge, + key: K ) { this.graph = graph; this.key = key; this.belongsToRelationship = belongsToRelationship; - this.type = belongsToRelationship.definition.type; + this.type = belongsToRelationship.definition.type as TypeFromInstanceOrString; this.store = store; this.___identifier = parentIdentifier; + this.___relatedToken = null; this.___token = store.notifications.subscribe( parentIdentifier, @@ -109,14 +147,14 @@ export default class BelongsToReference { * @public */ @cached - @dependentKeyCompat - get identifier(): StableRecordIdentifier | null { + @compat + get identifier(): StableRecordIdentifier> | null { if (this.___relatedToken) { this.store.notifications.unsubscribe(this.___relatedToken); this.___relatedToken = null; } - let resource = this._resource(); + const resource = this._resource(); if (resource && resource.data) { const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data); this.___relatedToken = this.store.notifications.subscribe( @@ -128,7 +166,7 @@ export default class BelongsToReference { } ); - return identifier; + return identifier as StableRecordIdentifier>; } return null; @@ -217,11 +255,11 @@ export default class BelongsToReference { @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship. */ link(): string | null { - let resource = this._resource(); + const resource = this._resource(); if (isResourceIdentiferWithRelatedLinks(resource)) { if (resource.links) { - let related = resource.links.related; + const related = resource.links.related; return !related || typeof related === 'string' ? related : related.href; } } @@ -233,10 +271,10 @@ export default class BelongsToReference { * * @method links * @public - * @returns + * @return */ links(): Links | null { - let resource = this._resource(); + const resource = this._resource(); return resource && resource.links ? resource.links : null; } @@ -281,9 +319,9 @@ export default class BelongsToReference { @public @return {Object} The meta information for the belongs-to relationship. */ - meta() { - let meta: Dict | null = null; - let resource = this._resource(); + meta(): Meta | null { + let meta: Meta | null = null; + const resource = this._resource(); if (resource && resource.meta && typeof resource.meta === 'object') { meta = resource.meta; } @@ -291,11 +329,12 @@ export default class BelongsToReference { } _resource() { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this._ref; // subscribe - const cache = DEPRECATE_V1_RECORD_DATA - ? this.store._instanceCache.getResourceCache(this.___identifier) - : this.store.cache; - return cache.getRelationship(this.___identifier, this.key) as SingleResourceRelationship; + const cache = this.store.cache; + return cache.getRelationship(this.___identifier, this.key) as SingleResourceRelationship< + StableRecordIdentifier> + >; } /** @@ -341,7 +380,7 @@ export default class BelongsToReference { @return {String} The name of the remote type. This should either be `link` or `id` */ remoteType(): 'link' | 'id' { - let value = this._resource(); + const value = this._resource(); if (isResourceIdentiferWithRelatedLinks(value)) { return 'link'; } @@ -349,11 +388,12 @@ export default class BelongsToReference { } /** - `push` can be used to update the data in the relationship and Ember - Data will treat the new data as the canonical value of this - relationship on the backend. + `push` can be used to update the data in the relationship and EmberData + will treat the new data as the canonical value of this relationship on + the backend. A value of `null` (e.g. `{ data: null }`) can be passed to + clear the relationship. - Example + Example model ```app/models/blog.js import Model, { belongsTo } from '@ember-data/model'; @@ -361,45 +401,94 @@ export default class BelongsToReference { export default class BlogModel extends Model { @belongsTo('user', { async: true, inverse: null }) user; } + ``` - let blog = store.push({ + Setup some initial state, note we haven't loaded the user yet: + + ```js + const blog = store.push({ data: { type: 'blog', - id: 1, + id: '1', relationships: { user: { - data: { type: 'user', id: 1 } + data: { type: 'user', id: '1' } } } } - }); - let userRef = blog.belongsTo('user'); + }); - // provide data for reference - userRef.push({ + const userRef = blog.belongsTo('user'); + userRef.id(); // '1' + ``` + + Update the state using `push`, note we can do this even without + having loaded the user yet by providing a resource-identifier. + + Both full a resource and a resource-identifier are supported. + + ```js + await userRef.push({ data: { type: 'user', - id: 1, - attributes: { - username: "@user" - } + id: '2', } - }).then(function(user) { - userRef.value() === user; }); + + userRef.id(); // '2' ``` + You may also pass in links and meta fore the relationship, and sideload + additional resources that might be required. + + ```js + await userRef.push({ + data: { + type: 'user', + id: '2', + }, + links: { + related: '/articles/1/author' + }, + meta: { + lastUpdated: Date.now() + }, + included: [ + { + type: 'user-preview', + id: '2', + attributes: { + username: '@runspired' + } + } + ] + }); + ``` + + By default, the store will attempt to fetch the record if it is not loaded or its + resource data is not included in the call to `push` before resolving the returned + promise with the new state.. + + Alternatively, pass `true` as the second argument to avoid fetching unloaded records + and instead the promise will resolve with void without attempting to fetch. This is + particularly useful if you want to update the state of the relationship without + forcing the load of all of the associated record. + @method push - @public - @param {Object|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship. - @return {Promise} A promise that resolves with the new value in this belongs-to relationship. - */ - async push(data: SingleResourceDocument | Promise): Promise { - let jsonApiDoc: SingleResourceDocument = data as SingleResourceDocument; + @public + @param {Object} doc a JSONAPI document object describing the new value of this relationship. + @param {Boolean} [skipFetch] if `true`, do not attempt to fetch unloaded records + @return {Promise} + */ + async push( + maybeDoc: SingleResourceDocument | Promise, + skipFetch?: boolean + ): Promise { + let doc: SingleResourceDocument = maybeDoc as SingleResourceDocument; if (DEPRECATE_PROMISE_PROXIES) { - if ((data as { then: unknown }).then) { - jsonApiDoc = await data; - if (jsonApiDoc !== data) { + 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, @@ -416,29 +505,44 @@ export default class BelongsToReference { } } } - let record = this.store.push(jsonApiDoc); + + const { store } = this; + const isResourceData = doc.data && isMaybeResource(doc.data); + const added = isResourceData + ? (store._push(doc, true) as StableExistingRecordIdentifier) + : doc.data + ? (store.identifierCache.getOrCreateRecordIdentifier(doc.data) as StableExistingRecordIdentifier) + : null; + const { identifier } = this.belongsToRelationship; if (DEBUG) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - assertPolymorphicType( - this.belongsToRelationship.identifier, - this.belongsToRelationship.definition, - recordIdentifierFor(record), - this.store - ); + if (added) { + assertPolymorphicType(identifier, this.belongsToRelationship.definition, added, store); + } } - const { identifier } = this.belongsToRelationship; - this.store._join(() => { + const newData: SingleResourceRelationship = {}; + + // only set data if it was passed in + if (doc.data || doc.data === null) { + newData.data = added; + } + if ('links' in doc) { + newData.links = doc.links; + } + if ('meta' in doc) { + newData.meta = doc.meta; + } + store._join(() => { this.graph.push({ - op: 'replaceRelatedRecord', + op: 'updateRelationship', record: identifier, field: this.key, - value: recordIdentifierFor(record), + value: newData, }); }); - return record; + if (!skipFetch) return this.load(); } /** @@ -491,8 +595,8 @@ export default class BelongsToReference { @public @return {Model} the record in this relationship */ - value(): RecordInstance | null { - let resource = this._resource(); + value(): Related | null { + const resource = this._resource(); return resource && resource.data ? this.store.peekRecord(resource.data) : null; } @@ -558,7 +662,7 @@ export default class BelongsToReference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the record in this belongs-to relationship. */ - load(options?: Dict) { + async load(options?: Record): Promise { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; @@ -566,7 +670,9 @@ export default class BelongsToReference { !this.belongsToRelationship.definition.isAsync && !areAllInverseRecordsLoaded(this.store, this._resource()); return fetchSyncRel ? support.reloadBelongsTo(this.key, options).then(() => this.value()) - : support.getBelongsTo(this.key, options); + : // we cast to fix the return type since typescript and eslint don't understand async functions + // properly + (support.getBelongsTo(this.key, options) as Promise); } /** @@ -619,10 +725,11 @@ export default class BelongsToReference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the record in this belongs-to relationship after the reload has completed. */ - reload(options?: Dict) { + reload(options?: Record) { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; return support.reloadBelongsTo(this.key, options).then(() => this.value()); } } +defineSignal(BelongsToReference.prototype, '_ref', 0); diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index 59300106929..b41ecfed519 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -1,34 +1,32 @@ import { deprecate } from '@ember/debug'; -import { dependentKeyCompat } from '@ember/object/compat'; -import { cached, tracked } from '@glimmer/tracking'; -import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; - -import { ManyArray } from 'ember-data/-private'; - -import { DEPRECATE_PROMISE_PROXIES, DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import type { Graph } from '@ember-data/graph/-private/graph/graph'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; +import type { CollectionEdge, Graph } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import { recordIdentifierFor } from '@ember-data/store'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; +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'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { CollectionResourceDocument, CollectionResourceRelationship, ExistingResourceObject, LinkObject, + Meta, PaginationLinks, - SingleResourceDocument, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; +} from '@warp-drive/core-types/spec/json-api-raw'; +import type { IsUnknown } from '../belongs-to'; import { assertPolymorphicType } from '../debug/assert-polymorphic-type'; -import { areAllInverseRecordsLoaded, LegacySupport } from '../legacy-relationships-support'; -import { LEGACY_SUPPORT } from '../model'; +import type { LegacySupport } from '../legacy-relationships-support'; +import { areAllInverseRecordsLoaded, LEGACY_SUPPORT } from '../legacy-relationships-support'; +import type { RelatedCollection as ManyArray } from '../many-array'; +import type { MaybeHasManyFields } from '../type-utils'; /** @module @ember-data/model @@ -37,47 +35,86 @@ interface ResourceIdentifier { links?: { related?: string | LinkObject; }; - meta?: JSONObject; + meta?: Meta; } +type ArrayItemType = T extends (infer U)[] ? U : never; + function isResourceIdentiferWithRelatedLinks( value: CollectionResourceRelationship | ResourceIdentifier | null ): value is ResourceIdentifier & { links: { related: string | LinkObject | null } } { return Boolean(value && value.links && value.links.related); } /** - A `HasManyReference` is a low-level API that allows users and addon - authors to perform meta-operations on a has-many relationship. + A `HasManyReference` is a low-level API that allows access + and manipulation of a hasMany relationship. + + It is especially useful when you're dealing with `async` relationships + from `@ember-data/model` as it allows synchronous access to + the relationship data if loaded, as well as APIs for loading, reloading + the data or accessing available information without triggering a load. + + It may also be useful when using `sync` relationships with `@ember-data/model` + that need to be loaded/reloaded with more precise timing than marking the + relationship as `async` and relying on autofetch would have allowed. + + However,keep in mind that marking a relationship as `async: false` will introduce + bugs into your application if the data is not always guaranteed to be available + by the time the relationship is accessed. Ergo, it is recommended when using this + approach to utilize `links` for unloaded relationship state instead of identifiers. + + Reference APIs are entangled with the relationship's underlying state, + thus any getters or cached properties that utilize these will properly + invalidate if the relationship state changes. + + References are "stable", meaning that multiple calls to retrieve the reference + for a given relationship will always return the same HasManyReference. @class HasManyReference @public - @extends Reference */ -export default class HasManyReference { +export default class HasManyReference< + T = unknown, + K extends string = IsUnknown extends true ? string : MaybeHasManyFields, + Related = K extends keyof T ? ArrayItemType> : unknown, +> { declare graph: Graph; - declare key: string; - declare hasManyRelationship: ManyRelationship; - declare type: string; declare store: Store; + declare hasManyRelationship: CollectionEdge; + /** + * The field name on the parent record for this has-many relationship. + * + * @property {String} key + * @public + */ + declare key: K; + + /** + * The type of resource this relationship will contain. + * + * @property {String} type + * @public + */ + declare type: TypeFromInstanceOrString; // unsubscribe tokens given to us by the notification manager - ___token!: Object; - ___identifier: StableRecordIdentifier; - ___relatedTokenMap!: Map; + ___token!: object; + ___identifier: StableRecordIdentifier>; + ___relatedTokenMap!: Map; - @tracked _ref = 0; + declare _ref: number; constructor( store: Store, graph: Graph, - parentIdentifier: StableRecordIdentifier, - hasManyRelationship: ManyRelationship, - key: string + parentIdentifier: StableRecordIdentifier>, + hasManyRelationship: CollectionEdge, + key: K ) { this.graph = graph; this.key = key; this.hasManyRelationship = hasManyRelationship; - this.type = hasManyRelationship.definition.type; + this.type = hasManyRelationship.definition.type as TypeFromInstanceOrString; this.store = store; this.___identifier = parentIdentifier; @@ -93,6 +130,11 @@ export default class HasManyReference { // TODO inverse } + /** + * This method should never be called by user code. + * + * @internal + */ destroy() { this.store.notifications.unsubscribe(this.___token); this.___relatedTokenMap.forEach((token) => { @@ -108,13 +150,14 @@ export default class HasManyReference { * @public */ @cached - @dependentKeyCompat - get identifiers(): StableRecordIdentifier[] { + @compat + get identifiers(): StableRecordIdentifier>[] { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this._ref; // consume the tracked prop - let resource = this._resource(); + const resource = this._resource(); - let map = this.___relatedTokenMap; + const map = this.___relatedTokenMap; this.___relatedTokenMap = new Map(); if (resource && resource.data) { @@ -136,7 +179,7 @@ export default class HasManyReference { } this.___relatedTokenMap.set(identifier, token); - return identifier; + return identifier as StableRecordIdentifier>; }); } @@ -149,9 +192,7 @@ export default class HasManyReference { } _resource() { - const cache = DEPRECATE_V1_RECORD_DATA - ? this.store._instanceCache.getResourceCache(this.___identifier) - : this.store.cache; + const cache = this.store.cache; return cache.getRelationship(this.___identifier, this.key) as CollectionResourceRelationship; } @@ -198,7 +239,7 @@ export default class HasManyReference { @return {String} The name of the remote type. This should either be `link` or `ids` */ remoteType(): 'link' | 'ids' { - let value = this._resource(); + const value = this._resource(); if (value && value.links && value.links.related) { return 'link'; } @@ -284,11 +325,11 @@ export default class HasManyReference { @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship. */ link(): string | null { - let resource = this._resource(); + const resource = this._resource(); if (isResourceIdentiferWithRelatedLinks(resource)) { if (resource.links) { - let related = resource.links.related; + const related = resource.links.related; return !related || typeof related === 'string' ? related : related.href; } } @@ -300,10 +341,10 @@ export default class HasManyReference { * * @method links * @public - * @returns + * @return */ links(): PaginationLinks | null { - let resource = this._resource(); + const resource = this._resource(); return resource && resource.links ? resource.links : null; } @@ -346,11 +387,11 @@ export default class HasManyReference { @method meta @public - @return {Object} The meta information for the belongs-to relationship. + @return {Object|null} The meta information for the belongs-to relationship. */ - meta() { - let meta: Dict | null = null; - let resource = this._resource(); + meta(): Meta | null { + let meta: Meta | null = null; + const resource = this._resource(); if (resource && resource.meta && typeof resource.meta === 'object') { meta = resource.meta; } @@ -358,11 +399,12 @@ export default class HasManyReference { } /** - `push` can be used to update the data in the relationship and Ember - Data will treat the new data as the canonical value of this - relationship on the backend. + `push` can be used to update the data in the relationship and EmberData + will treat the new data as the canonical value of this relationship on + the backend. An empty array will signify the canonical value should be + empty. - Example + Example model ```app/models/post.js import Model, { hasMany } from '@ember-data/model'; @@ -372,46 +414,97 @@ export default class HasManyReference { } ``` - ``` - let post = store.push({ + Setup some initial state, note we haven't loaded the comments yet: + + ```js + const post = store.push({ data: { type: 'post', - id: 1, + id: '1', relationships: { comments: { - data: [{ type: 'comment', id: 1 }] + data: [{ type: 'comment', id: '1' }] } } } }); - let commentsRef = post.hasMany('comments'); - + const commentsRef = post.hasMany('comments'); commentsRef.ids(); // ['1'] + ``` + + Update the state using `push`, note we can do this even without + having loaded these comments yet by providing resource identifiers. - commentsRef.push([ - [{ type: 'comment', id: 2 }], - [{ type: 'comment', id: 3 }], - ]) + Both full resources and resource identifiers are supported. + + ```js + await commentsRef.push({ + data: [ + { type: 'comment', id: '2' }, + { type: 'comment', id: '3' }, + ] + }); commentsRef.ids(); // ['2', '3'] ``` + For convenience, you can also pass in an array of resources or resource identifiers + without wrapping them in the `data` property: + + ```js + await commentsRef.push([ + { type: 'comment', id: '4' }, + { type: 'comment', id: '5' }, + ]); + + commentsRef.ids(); // ['4', '5'] + ``` + + When using the `data` property, you may also include other resource data via included, + as well as provide new links and meta to the relationship. + + ```js + await commentsRef.push({ + links: { + related: '/posts/1/comments' + }, + meta: { + total: 2 + }, + data: [ + { type: 'comment', id: '4' }, + { type: 'comment', id: '5' }, + ], + included: [ + { type: 'other-thing', id: '1', attributes: { foo: 'bar' }, + ] + }); + ``` + + By default, the store will attempt to fetch any unloaded records before resolving + the returned promise with the ManyArray. + + Alternatively, pass `true` as the second argument to avoid fetching unloaded records + and instead the promise will resolve with void without attempting to fetch. This is + particularly useful if you want to update the state of the relationship without + forcing the load of all of the associated records. + @method push - @public - @param {Array|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship. - @return {ManyArray} - */ + @public + @param {Array|Object} doc a JSONAPI document object describing the new value of this relationship. + @param {Boolean} [skipFetch] if `true`, do not attempt to fetch unloaded records + @return {Promise} + */ async push( - objectOrPromise: ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] } - ): Promise { - let payload = objectOrPromise; + maybeDoc: ExistingResourceObject[] | CollectionResourceDocument, + skipFetch?: boolean + ): Promise | void> { + let doc = maybeDoc; if (DEPRECATE_PROMISE_PROXIES) { - if ((objectOrPromise as unknown as { then: unknown }).then) { - payload = await (objectOrPromise as unknown as Promise< - ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] } - >); - if (payload !== objectOrPromise) { + 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, @@ -428,57 +521,64 @@ export default class HasManyReference { } } } - let array: Array; - - if (!Array.isArray(payload) && typeof payload === 'object' && Array.isArray(payload.data)) { - array = payload.data; - } else { - array = payload as ExistingResourceObject[]; - } 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]); - let identifiers = array.map((obj) => { - let record: RecordInstance; - if ('data' in obj) { - // TODO deprecate pushing non-valid JSON:API here - record = store.push(obj); - } else { - record = store.push({ data: obj }); - } + // enforce that one of links, meta or data is present + assert( + `You must provide at least one of 'links', 'meta' or 'data' when calling hasManyReference.push`, + 'links' in dataDoc || 'meta' in dataDoc || 'data' in dataDoc + ); - if (DEBUG) { - let relationshipMeta = this.hasManyRelationship.definition; - let identifier = this.hasManyRelationship.identifier; + const identifiers = !Array.isArray(dataDoc.data) + ? [] + : isResourceData + ? (store._push(dataDoc, true) as StableRecordIdentifier[]) + : dataDoc.data.map((i) => store.identifierCache.getOrCreateRecordIdentifier(i)); + const { identifier } = this.hasManyRelationship; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - assertPolymorphicType(identifier, relationshipMeta, recordIdentifierFor(record), store); - } - return recordIdentifierFor(record); - }); + if (DEBUG) { + const relationshipMeta = this.hasManyRelationship.definition; - const { identifier } = this.hasManyRelationship; + identifiers.forEach((added) => { + assertPolymorphicType(identifier, relationshipMeta, added, store); + }); + } + + const newData: CollectionResourceRelationship = {}; + // only set data if it was passed in + if (Array.isArray(dataDoc.data)) { + newData.data = identifiers; + } + if ('links' in dataDoc) { + newData.links = dataDoc.links; + } + if ('meta' in dataDoc) { + newData.meta = dataDoc.meta; + } store._join(() => { this.graph.push({ - op: 'replaceRelatedRecords', + op: 'updateRelationship', record: identifier, field: this.key, - value: identifiers, + value: newData, }); }); - return this.load(); + if (!skipFetch) return this.load(); } _isLoaded() { - let hasRelationshipDataProperty = this.hasManyRelationship.state.hasReceivedData; + const hasRelationshipDataProperty = this.hasManyRelationship.state.hasReceivedData; if (!hasRelationshipDataProperty) { return false; } - let localState = this.hasManyRelationship.localState; + const relationship = this.graph.getData(this.hasManyRelationship.identifier, this.key) as CollectionRelationship; - return localState.every((identifier) => { + return relationship.data?.every((identifier) => { return this.store._instanceCache.recordIsLoaded(identifier, true) === true; }); } @@ -524,7 +624,7 @@ export default class HasManyReference { @public @return {ManyArray} */ - value() { + value(): ManyArray | null { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; @@ -534,11 +634,12 @@ export default class HasManyReference { 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; } - return support.getManyArray(this.key); + return support.getManyArray(this.key); } /** @@ -600,20 +701,22 @@ export default class HasManyReference { ``` @method load - @public + @public @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - async load(options?: FindOptions): Promise { + async load(options?: BaseFinderOptions): Promise> { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; const fetchSyncRel = !this.hasManyRelationship.definition.isAsync && !areAllInverseRecordsLoaded(this.store, this._resource()); return fetchSyncRel - ? (support.reloadHasMany(this.key, options) as Promise) - : (support.getHasMany(this.key, options) as Promise | ManyArray); // this cast is necessary because typescript does not work properly with custom thenables; + ? (support.reloadHasMany(this.key, options) as Promise>) + : // we cast to fix the return type since typescript and eslint don't understand async functions + // properly + (support.getHasMany(this.key, options) as Promise> | ManyArray); } /** @@ -666,10 +769,16 @@ export default class HasManyReference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - reload(options?: FindOptions) { + reload(options?: BaseFinderOptions): Promise> { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; - return support.reloadHasMany(this.key, options); + return support.reloadHasMany(this.key, options) as Promise>; } } +defineSignal(HasManyReference.prototype, '_ref', 0); + +export function isMaybeResource(object: ExistingResourceObject | ResourceIdentifier): object is ExistingResourceObject { + const keys = Object.keys(object).filter((k) => k !== 'id' && k !== 'type' && k !== 'lid'); + return keys.length > 0; +} diff --git a/packages/model/src/-private/relationship-meta.ts b/packages/model/src/-private/relationship-meta.ts index c2cb75517b1..e03a181c389 100644 --- a/packages/model/src/-private/relationship-meta.ts +++ b/packages/model/src/-private/relationship-meta.ts @@ -1,13 +1,12 @@ -import { dasherize } from '@ember/string'; - -import { singularize } from 'ember-inflector'; - -import { DEBUG } from '@ember-data/env'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; import type Store from '@ember-data/store'; -import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; +import { DEBUG } from '@warp-drive/build-config/env'; +import type { LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; -function typeForRelationshipMeta(meta) { - let modelName = dasherize(meta.type || meta.key); +import type { Model } from './model'; + +function typeForRelationshipMeta(meta: LegacyRelationshipSchema): string { + let modelName = dasherize(meta.type || meta.name); if (meta.kind === 'hasMany') { modelName = singularize(modelName); @@ -16,34 +15,27 @@ function typeForRelationshipMeta(meta) { return modelName; } -function shouldFindInverse(relationshipMeta) { - let options = relationshipMeta.options; +function shouldFindInverse(relationshipMeta: LegacyRelationshipSchema): boolean { + const options = relationshipMeta.options; return !(options && options.inverse === null); } -class RelationshipDefinition implements RelationshipSchema { +class RelationshipDefinition { declare _type: string; - declare __inverseKey: string; + declare __inverseKey: string | null; declare __hasCalculatedInverse: boolean; declare parentModelName: string; declare inverseIsAsync: string | null; - declare meta: any; + declare meta: LegacyRelationshipSchema; - constructor(meta: any) { + constructor(meta: LegacyRelationshipSchema, parentModelName: string) { this._type = ''; this.__inverseKey = ''; this.__hasCalculatedInverse = false; - this.parentModelName = meta.parentModelName; + this.parentModelName = parentModelName; this.meta = meta; } - /** - * @internal - * @deprecated - */ - get key(): string { - return this.meta.key; - } get kind(): 'belongsTo' | 'hasMany' { return this.meta.kind; } @@ -54,32 +46,32 @@ class RelationshipDefinition implements RelationshipSchema { this._type = typeForRelationshipMeta(this.meta); return this._type; } - get options(): { [key: string]: any } { + get options() { return this.meta.options; } get name(): string { return this.meta.name; } - _inverseKey(store: Store, modelClass): string { + _inverseKey(store: Store, modelClass: typeof Model): string | null { if (this.__hasCalculatedInverse === false) { this._calculateInverse(store, modelClass); } return this.__inverseKey; } - _calculateInverse(store: Store, modelClass): void { + _calculateInverse(store: Store, modelClass: typeof Model): void { this.__hasCalculatedInverse = true; - let inverseKey; - let inverse: any = null; + let inverseKey: string | null = null; + let inverse: LegacyRelationshipSchema | null = null; if (shouldFindInverse(this.meta)) { - inverse = modelClass.inverseFor(this.key, store); + 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.key, store); + modelClass.typeForRelationship(this.name, store); } } @@ -93,6 +85,9 @@ class RelationshipDefinition implements RelationshipSchema { } export type { RelationshipDefinition }; -export function relationshipFromMeta(meta: RelationshipSchema): RelationshipDefinition { - return new RelationshipDefinition(meta); +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 new file mode 100644 index 00000000000..d906b9b29f2 --- /dev/null +++ b/packages/model/src/-private/schema-provider.ts @@ -0,0 +1,318 @@ +import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; + +import type Store from '@ember-data/store'; +import type { SchemaService } from '@ember-data/store/types'; +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'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + GenericField, + HashField, + LegacyAttributeField, + LegacyFieldSchema, + LegacyRelationshipSchema, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; + +import type { FactoryCache, Model, ModelFactory, ModelStore } from './model'; +import _modelForMixin from './model-for-mixin'; +import { normalizeModelName } from './util'; + +type AttributesSchema = ReturnType>; +type RelationshipsSchema = ReturnType>; + +type InternalSchema = { + schema: ResourceSchema; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export interface ModelSchemaProvider { + attributesDefinitionFor(resource: RecordIdentifier | { type: string }): AttributesSchema; + + relationshipsDefinitionFor(resource: RecordIdentifier | { type: string }): RelationshipsSchema; + + doesTypeExist(type: string): boolean; +} +export class ModelSchemaProvider implements SchemaService { + declare store: ModelStore; + declare _schemas: Map; + declare _typeMisses: Set; + + constructor(store: ModelStore) { + this.store = store; + this._schemas = new Map(); + this._typeMisses = new Set(); + } + + hasTrait(type: string): boolean { + assert(`hasTrait is not available with @ember-data/model's SchemaService`); + return false; + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + assert(`resourceHasTrait is not available with @ember-data/model's SchemaService`); + return false; + } + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + assert(`transformation is not available with @ember-data/model's SchemaService`); + } + derivation(field: DerivedField | { type: string }): Derivation { + assert(`derivation is not available with @ember-data/model's SchemaService`); + } + hashFn(field: HashField | { type: string }): HashFn { + assert(`hashFn is not available with @ember-data/model's SchemaService`); + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + const type = normalizeModelName(resource.type); + + if (!this._schemas.has(type)) { + this._loadModelSchema(type); + } + + return this._schemas.get(type)!.schema; + } + registerResources(schemas: ResourceSchema[]): void { + assert(`registerResources is not available with @ember-data/model's SchemaService`); + } + registerResource(schema: ResourceSchema): void { + assert(`registerResource is not available with @ember-data/model's SchemaService`); + } + registerTransformation(transform: Transformation): void { + assert(`registerTransformation is not available with @ember-data/model's SchemaService`); + } + registerDerivation(derivation: Derivation): void { + assert(`registerDerivation is not available with @ember-data/model's SchemaService`); + } + registerHashFn(hashFn: HashFn): void { + assert(`registerHashFn is not available with @ember-data/model's SchemaService`); + } + _loadModelSchema(type: string) { + const modelClass = this.store.modelFor(type) as typeof Model; + const attributeMap = modelClass.attributes; + + const attributes = Object.create(null) as AttributesSchema; + attributeMap.forEach((meta, name) => (attributes[name] = meta)); + const relationships = modelClass.relationshipsObject || null; + const fields = new Map(); + + for (const attr of Object.values(attributes)) { + fields.set(attr.name, attr); + } + + for (const rel of Object.values(relationships)) { + fields.set(rel.name, rel); + } + + const schema: ResourceSchema = { + legacy: true, + identity: { name: 'id', kind: '@id' }, + type, + fields: Array.from(fields.values()), + }; + + const internalSchema: InternalSchema = { + schema, + attributes, + relationships, + fields, + }; + + this._schemas.set(type, internalSchema); + + return internalSchema; + } + + fields(resource: RecordIdentifier | { type: string }): Map { + const type = normalizeModelName(resource.type); + + if (!this._schemas.has(type)) { + this._loadModelSchema(type); + } + + return this._schemas.get(type)!.fields; + } + + hasResource(resource: { type: string }): boolean { + const type = normalizeModelName(resource.type); + + if (this._schemas.has(type)) { + return true; + } + + if (this._typeMisses.has(type)) { + return false; + } + + const factory = getModelFactory(this.store, type); + const exists = factory !== null; + + if (!exists) { + this._typeMisses.add(type); + return false; + } + + return true; + } +} + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + ModelSchemaProvider.prototype.doesTypeExist = function (type: string): boolean { + 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 { + 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); + } + + return this._schemas.get(type)!.attributes; + }; + + ModelSchemaProvider.prototype.relationshipsDefinitionFor = function ( + resource: RecordIdentifier | { type: string } + ): RelationshipsSchema { + 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); + } + + return this._schemas.get(type)!.relationships; + }; +} + +export function buildSchema(store: Store): SchemaService { + return new ModelSchemaProvider(store as ModelStore); +} + +export function getModelFactory(store: ModelStore, type: string): ModelFactory | null { + if (!store._modelFactoryCache) { + store._modelFactoryCache = Object.create(null) as FactoryCache; + } + const cache = store._modelFactoryCache; + let factory: ModelFactory | undefined = cache[type]; + + if (!factory) { + const owner = getOwner(store)!; + factory = owner.factoryFor(`model:${type}`) as ModelFactory | undefined; + + if (!factory) { + //Support looking up mixins as base types for polymorphic relationships + factory = _modelForMixin(store, type); + } + + if (!factory) { + // we don't cache misses in case someone wants to register a missing model + return null; + } + + const klass = factory.class; + + if (klass.isModel) { + const hasOwnModelNameSet = klass.modelName && Object.prototype.hasOwnProperty.call(klass, 'modelName'); + if (!hasOwnModelNameSet) { + Object.defineProperty(klass, 'modelName', { value: type }); + } + } + + cache[type] = factory; + } + + return factory; +} diff --git a/packages/model/src/-private/type-utils.ts b/packages/model/src/-private/type-utils.ts new file mode 100644 index 00000000000..4f9af2a92e0 --- /dev/null +++ b/packages/model/src/-private/type-utils.ts @@ -0,0 +1,70 @@ +import type { TypedRecordInstance } from '@warp-drive/core-types/record'; +import type { Type } from '@warp-drive/core-types/symbols'; + +import type { RelatedCollection } from './many-array'; +import type { Model } from './model'; +import type { PromiseBelongsTo } from './promise-belongs-to'; +import type { PromiseManyArray } from './promise-many-array'; + +type ExcludeNull = Exclude extends never ? T : Exclude; +type GetMappedKey = { [K in keyof M]-?: ExcludeNull extends V ? K : never }[keyof M] & string; + +/** + * Get the keys of fields that are maybe defined as `belongsTo` relationships + * + * "Maybe" because getter/computed fields might be returning values that look + * like relationships, but are not. + * + * @typedoc + */ +export type MaybeBelongsToFields = + _TrueKeys extends never ? string : _MaybeBelongsToFields; +export type _MaybeBelongsToFields = GetMappedKey; + +/** + * Get the keys of fields that are maybe defined as `hasMany` relationships + * + * "Maybe" because getter/computed fields might be returning values that look + * like relationships, but are not. + * + * @typedoc + */ +export type MaybeHasManyFields = _TrueKeys extends never ? string : _MaybeHasManyFields; +type _MaybeHasManyFields = GetMappedKey; + +/** + * Get the keys of fields that are maybe defined as `attr` fields + * + * "Maybe" because getter/computed fields might be returning values that look + * like attributes, but are not. + * + * This is computed by excluding the keys that are defined as `belongsTo` or `hasMany` + * as well as the keys on EmberObject and the Model base class + * + * @typedoc + */ +export type MaybeAttrFields = Exclude< + _TrueKeys, + _MaybeBelongsToFields | _MaybeHasManyFields +>; + +/** + * Get the keys of fields that are maybe defined as relationships + * + * "Maybe" because getter/computed fields might be returning values that look + * like relationships, but are not. + * + * @typedoc + */ +export type MaybeRelationshipFields = + _TrueKeys extends never ? string : _MaybeBelongsToFields | _MaybeHasManyFields; + +type _TrueKeys = Exclude; +export type isSubClass = _TrueKeys extends never ? false : true; +/** + * Get the keys of all fields defined on the given subclass of Model + * that don't exist on EmberObject or Model. + * + * @typedoc + */ +export type SubclassKeys = _TrueKeys extends never ? string : _TrueKeys; diff --git a/packages/model/src/-private/util.ts b/packages/model/src/-private/util.ts index 42b90e43679..b9f0380dde3 100644 --- a/packages/model/src/-private/util.ts +++ b/packages/model/src/-private/util.ts @@ -1,7 +1,12 @@ -export type DecoratorPropertyDescriptor = (PropertyDescriptor & { initializer?: any }) | undefined; +import { deprecate } from '@ember/debug'; -export function isElementDescriptor(args: any[]): args is [object, string, DecoratorPropertyDescriptor] { - let [maybeTarget, maybeKey, maybeDesc] = args; +import { dasherize } from '@ember-data/request-utils/string'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + +export type DecoratorPropertyDescriptor = (PropertyDescriptor & { initializer?: () => unknown }) | undefined; + +export function isElementDescriptor(args: unknown[]): args is [object, string, DecoratorPropertyDescriptor] { + const [maybeTarget, maybeKey, maybeDesc] = args; return ( // Ensure we have the right number of args @@ -20,6 +25,26 @@ export function isElementDescriptor(args: any[]): args is [object, string, Decor ); } -export function computedMacroWithOptionalParams(fn) { - return (...maybeDesc: any[]) => (isElementDescriptor(maybeDesc) ? fn()(...maybeDesc) : fn(...maybeDesc)); +export function normalizeModelName(type: string): string { + if (DEPRECATE_NON_STRICT_TYPES) { + const result = 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; } diff --git a/packages/model/src/hooks.ts b/packages/model/src/hooks.ts new file mode 100644 index 00000000000..1c7335ed6b2 --- /dev/null +++ b/packages/model/src/hooks.ts @@ -0,0 +1,2 @@ +export { instantiateRecord, teardownRecord, modelFor } from './-private/hooks'; +export { buildSchema } from './-private/schema-provider'; diff --git a/packages/model/src/index.ts b/packages/model/src/index.ts index c06af98de24..35bc6905205 100644 --- a/packages/model/src/index.ts +++ b/packages/model/src/index.ts @@ -38,3 +38,10 @@ @main @ember-data/model */ export { Model as default, attr, belongsTo, hasMany } from './-private'; + +export type { PromiseBelongsTo as AsyncBelongsTo } from './-private/promise-belongs-to'; +export type { PromiseManyArray as AsyncHasMany } from './-private/promise-many-array'; +export type { RelatedCollection as ManyArray } from './-private/many-array'; +export type { RelatedCollection as HasMany } from './-private/many-array'; +export { instantiateRecord, teardownRecord, modelFor } from './-private/hooks'; +export { ModelSchemaProvider } from './-private/schema-provider'; diff --git a/packages/model/src/migration-support.ts b/packages/model/src/migration-support.ts new file mode 100644 index 00000000000..4ade9960611 --- /dev/null +++ b/packages/model/src/migration-support.ts @@ -0,0 +1,268 @@ +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { SchemaService } from '@ember-data/store/types'; +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; +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 { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; +import type { WithPartial } from '@warp-drive/core-types/utils'; + +import { Errors } from './-private'; +import type { MinimalLegacyRecord } from './-private/model-methods'; +import { + belongsTo, + changedAttributes, + createSnapshot, + deleteRecord, + destroyRecord, + hasMany, + reload, + rollbackAttributes, + save, + serialize, + unloadRecord, +} from './-private/model-methods'; +import RecordState from './-private/record-state'; +import { buildSchema } from './hooks'; + +type AttributesSchema = ReturnType>; +type RelationshipsSchema = ReturnType>; + +// 'isDestroying', 'isDestroyed' +const LegacyFields = [ + '_createSnapshot', + 'adapterError', + 'belongsTo', + 'changedAttributes', + 'constructor', + 'currentState', + 'deleteRecord', + 'destroyRecord', + 'dirtyType', + 'errors', + 'hasDirtyAttributes', + 'hasMany', + 'isDeleted', + 'isEmpty', + 'isError', + 'isLoaded', + 'isLoading', + 'isNew', + 'isSaving', + 'isValid', + 'reload', + 'rollbackAttributes', + 'save', + 'serialize', + 'unloadRecord', +]; + +const LegacySupport = getOrSetGlobal('LegacySupport', new WeakMap>()); + +function legacySupport(record: MinimalLegacyRecord, options: ObjectValue | null, prop: string): unknown { + let state = LegacySupport.get(record); + if (!state) { + state = {}; + LegacySupport.set(record, state); + } + + switch (prop) { + case '_createSnapshot': + return createSnapshot; + case 'adapterError': + return record.currentState.adapterError; + case 'belongsTo': + return belongsTo; + case 'changedAttributes': + return changedAttributes; + case 'constructor': + return (state._constructor = state._constructor || { + isModel: true, + name: `Record<${recordIdentifierFor(record).type}>`, + modelName: recordIdentifierFor(record).type, + }); + case 'currentState': + return (state.recordState = state.recordState || new RecordState(record)); + case 'deleteRecord': + return deleteRecord; + case 'destroyRecord': + return destroyRecord; + case 'dirtyType': + return record.currentState.dirtyType; + case 'errors': + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + return (state.errors = state.errors || Errors.create({ __record: record })); + case 'hasDirtyAttributes': + return record.currentState.isDirty; + case 'hasMany': + return hasMany; + case 'isDeleted': + return record.currentState.isDeleted; + case 'isEmpty': + return record.currentState.isEmpty; + case 'isError': + return record.currentState.isError; + case 'isLoaded': + return record.currentState.isLoaded; + case 'isLoading': + return record.currentState.isLoading; + case 'isNew': + return record.currentState.isNew; + case 'isSaving': + return record.currentState.isSaving; + case 'isValid': + return record.currentState.isValid; + case 'reload': + return reload; + case 'rollbackAttributes': + return rollbackAttributes; + case 'save': + return save; + case 'serialize': + return serialize; + case 'unloadRecord': + return unloadRecord; + default: + assert(`${prop} is not a supported legacy field`, false); + } +} +legacySupport[Type] = '@legacy'; + +export function withDefaults(schema: WithPartial): ResourceSchema { + schema.legacy = true; + schema.identity = { kind: '@id', name: 'id' }; + + LegacyFields.forEach((field) => { + schema.fields.push({ + type: '@legacy', + name: field, + kind: 'derived', + }); + }); + schema.fields.push({ + name: 'isReloading', + kind: '@local', + type: 'boolean', + options: { defaultValue: false }, + }); + schema.fields.push({ + name: 'isDestroying', + kind: '@local', + type: 'boolean', + options: { defaultValue: false }, + }); + schema.fields.push({ + name: 'isDestroyed', + kind: '@local', + type: 'boolean', + options: { defaultValue: false }, + }); + return schema as ResourceSchema; +} + +export function registerDerivations(schema: SchemaService) { + schema.registerDerivation(legacySupport); +} + +export interface DelegatingSchemaService { + attributesDefinitionFor?(resource: StableRecordIdentifier | { type: string }): AttributesSchema; + relationshipsDefinitionFor?(resource: StableRecordIdentifier | { type: string }): RelationshipsSchema; + doesTypeExist?(type: string): boolean; +} +export class DelegatingSchemaService implements SchemaService { + _preferred!: SchemaService; + _secondary!: SchemaService; + + constructor(store: Store, schema: SchemaService) { + this._preferred = schema; + this._secondary = buildSchema(store); + } + hasResource(resource: StableRecordIdentifier | { type: string }): boolean { + return this._preferred.hasResource(resource) || this._secondary.hasResource(resource); + } + hasTrait(type: string): boolean { + if (this._preferred.hasResource({ type })) { + return this._preferred.hasTrait(type); + } + return this._secondary.hasTrait(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + if (this._preferred.hasResource(resource)) { + return this._preferred.resourceHasTrait(resource, trait); + } + return this._secondary.resourceHasTrait(resource, trait); + } + fields(resource: StableRecordIdentifier | { type: string }): Map { + if (this._preferred.hasResource(resource)) { + return this._preferred.fields(resource); + } + return this._secondary.fields(resource); + } + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + return this._preferred.transformation(field); + } + hashFn(field: HashField | { type: string }): HashFn { + return this._preferred.hashFn(field); + } + derivation(field: DerivedField | { type: string }): Derivation { + return this._preferred.derivation(field); + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + if (this._preferred.hasResource(resource)) { + return this._preferred.resource(resource); + } + return this._secondary.resource(resource); + } + registerResources(schemas: ResourceSchema[]): void { + this._preferred.registerResources(schemas); + } + registerResource(schema: ResourceSchema): void { + this._preferred.registerResource(schema); + } + registerTransformation(transform: Transformation): void { + this._preferred.registerTransformation(transform); + } + registerDerivation(derivation: Derivation): void { + this._preferred.registerDerivation(derivation); + } + registerHashFn(hashFn: HashFn): void { + this._preferred.registerHashFn(hashFn); + } +} + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + DelegatingSchemaService.prototype.attributesDefinitionFor = function ( + resource: StableRecordIdentifier | { type: string } + ) { + if (this._preferred.hasResource(resource)) { + return this._preferred.attributesDefinitionFor!(resource); + } + + return this._secondary.attributesDefinitionFor!(resource); + }; + DelegatingSchemaService.prototype.relationshipsDefinitionFor = function ( + resource: StableRecordIdentifier | { type: string } + ) { + if (this._preferred.hasResource(resource)) { + return this._preferred.relationshipsDefinitionFor!(resource); + } + + return this._secondary.relationshipsDefinitionFor!(resource); + }; + DelegatingSchemaService.prototype.doesTypeExist = function (type: string) { + return this._preferred.doesTypeExist?.(type) || this._secondary.doesTypeExist?.(type) || false; + }; +} diff --git a/packages/model/tsconfig.json b/packages/model/tsconfig.json new file mode 100644 index 00000000000..e5ec4f5ba4c --- /dev/null +++ b/packages/model/tsconfig.json @@ -0,0 +1,81 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "allowJs": false, + "emitDeclarationOnly": true, + "noImplicitAny": false, + "noImplicitOverride": false, + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/graph": ["../graph/unstable-preview-types"], + "@ember-data/graph/*": ["../graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../legacy-compat/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../graph" + }, + { + "path": "../json-api" + }, + { + "path": "../legacy-compat" + }, + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/model/vite.config.mjs b/packages/model/vite.config.mjs new file mode 100644 index 00000000000..c06cb9e5c61 --- /dev/null +++ b/packages/model/vite.config.mjs @@ -0,0 +1,26 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + 'ember', + '@ember/service', + '@ember/debug', + '@ember/object/computed', + '@ember/object/internals', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/array', + '@ember/array/proxy', + '@ember/object', + '@ember/object/mixin', + '@ember/application', + '@ember/polyfills', +]; +export const entryPoints = ['src/index.ts', 'src/-private.ts', 'src/hooks.ts', 'src/migration-support.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/private-build-infra/.npmignore b/packages/private-build-infra/.npmignore deleted file mode 100644 index ee47bc4193c..00000000000 --- a/packages/private-build-infra/.npmignore +++ /dev/null @@ -1,19 +0,0 @@ -# compiled output -/dist/ -/tmp/ -/types/ - -# misc -/.bowerrc -/.editorconfig -/.env* -/.gitignore -/.watchmanconfig -/CONTRIBUTING.md -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-data -/node-tests \ No newline at end of file diff --git a/packages/private-build-infra/README.md b/packages/private-build-infra/README.md deleted file mode 100644 index acb84c1d340..00000000000 --- a/packages/private-build-infra/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# @ember-data/private-build-infra - -!! This is an internal package for use by `@ember-data` only. !! - -This package provides utilities for configuring addon-build setup -for `@ember-data/` packages. It is directly depended upon by those -packages and should not be installed for use in an app directly. - -## License - -This project is licensed under the [MIT License](LICENSE.md). diff --git a/packages/private-build-infra/package.json b/packages/private-build-infra/package.json deleted file mode 100644 index 823bcbf71b7..00000000000 --- a/packages/private-build-infra/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@ember-data/private-build-infra", - "version": "4.12.8", - "description": "The default blueprint for ember-data private packages.", - "repository": { - "type": "git", - "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "packages/private-build-infra" - }, - "license": "MIT", - "author": "", - "directories": {}, - "scripts": {}, - "dependencies": { - "@babel/core": "^7.21.4", - "@babel/plugin-transform-block-scoping": "^7.21.0", - "@babel/runtime": "^7.21.0", - "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "babel-import-util": "^1.3.0", - "babel-plugin-debug-macros": "^0.3.4", - "babel-plugin-filter-imports": "^4.0.0", - "babel6-plugin-strip-class-callcheck": "^6.0.0", - "broccoli-debug": "^0.6.5", - "broccoli-file-creator": "^2.1.1", - "broccoli-funnel": "^3.0.8", - "broccoli-merge-trees": "^4.2.0", - "broccoli-rollup": "^5.0.0", - "calculate-cache-key-for-tree": "^2.0.0", - "chalk": "^4.1.2", - "ember-cli-babel": "^7.26.11", - "ember-cli-path-utils": "^1.0.0", - "ember-cli-string-utils": "^1.1.0", - "ember-cli-version-checker": "^5.1.2", - "git-repo-info": "^2.1.1", - "glob": "^9.3.4", - "npm-git-info": "^1.0.3", - "semver": "^7.3.8", - "silent-error": "^1.1.1" - }, - "engines": { - "node": "16.* || >= 18.*" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/packages/private-build-infra/src/addon-build-config-for-data-package.js b/packages/private-build-infra/src/addon-build-config-for-data-package.js deleted file mode 100644 index 5dc806359c7..00000000000 --- a/packages/private-build-infra/src/addon-build-config-for-data-package.js +++ /dev/null @@ -1,315 +0,0 @@ -const calculateCacheKeyForTree = require('calculate-cache-key-for-tree'); -const Funnel = require('broccoli-funnel'); -const merge = require('broccoli-merge-trees'); -const BroccoliDebug = require('broccoli-debug'); -const VersionChecker = require('ember-cli-version-checker'); - -const rollupPrivateModule = require('./utilities/rollup-private-module'); -const detectModule = require('./utilities/detect-module'); - -function isProductionEnv() { - let isProd = /production/.test(process.env.EMBER_ENV); - let isTest = process.env.EMBER_CLI_TEST_COMMAND; - - return isProd && !isTest; -} - -function addonBuildConfigForDataPackage(pkg) { - return { - name: pkg.name, - - init() { - this._super.init && this._super.init.apply(this, arguments); - // console.log( - // 'init: ' + - // this.name + - // ' for ' + - // (typeof this.parent.name === 'function' ? this.parent.name() : this.parent.name) - // ); - this._prodLikeWarning(); - this.debugTree = BroccoliDebug.buildDebugCallback(`ember-data:${pkg.name}`); - this.options = this.options || {}; - Object.assign(this.options, { - '@embroider/macros': { - setOwnConfig: {}, - }, - 'ember-cli-babel': { - enableTypeScriptTransform: true, - }, - autoImport: { - exclude: [ - '@ember/string', - 'ember-inflector', - '@ember-data/store', - '@ember-data/adapter', - '@ember-data/serializer', - '@ember-data/request', - '@ember-data/model', - '@ember-data/json-api', - '@ember-data/debug', - '@ember-data/tracking', - '@glimmer/tracking', - ], - }, - }); - }, - - _prodLikeWarning() { - let emberEnv = process.env.EMBER_ENV; - if (emberEnv !== 'production' && /production/.test(emberEnv)) { - this._warn( - `Production-like values for EMBER_ENV are deprecated (your EMBER_ENV is "${emberEnv}") and support will be removed in Ember Data 4.0.0. If using ember-cli-deploy, please configure your build using 'production'. Otherwise please set your EMBER_ENV to 'production' for production builds.` - ); - } - }, - - isDevelopingAddon() { - if (typeof this.parent.name === 'string' && this.parent.name === 'ember-data') { - return this.parent.isDevelopingAddon(); - } - return this._super(...arguments); - }, - - _warn(message) { - let chalk = require('chalk'); - let warning = chalk.yellow('WARNING: ' + message); - - if (this.ui && this.ui.writeWarnLine) { - this.ui.writeWarnLine(message); - } else if (this.ui) { - this.ui.writeLine(warning); - } else { - // eslint-disable-next-line no-console - console.log(warning); - } - }, - - _suppressUneededRollupWarnings(message, next) { - if (message.code === 'CIRCULAR_DEPENDENCY') { - return; - } else if (message.code === 'NON_EXISTENT_EXPORT') { - // ignore type imports - if (message.message.indexOf(`@ember-data/types`) !== -1) { - return; - } - } else if (message.code === 'UNRESOLVED_IMPORT') { - if (!this.isDevelopingAddon()) { - // don't print these for consumers - return; - } else { - const chalk = require('chalk'); - // make warning actionable - // eslint-disable-next-line no-console - console.log( - chalk.yellow( - `\n\n⚠️ Add ${chalk.white( - message.source - )} to the array returned by externalDependenciesForPrivateModule in index.js of ${chalk.white( - this.name - )}\n\n` - ) - ); - throw message.message; - } - } - next(message); - }, - - shouldIncludeChildAddon(addon) { - if (addon.name.startsWith('@ember-data')) { - if (this.name === 'ember-data' || addon.name === '@ember-data/tracking') { - return true; - } - - return false; - } - return true; - }, - - getOutputDirForVersion() { - let VersionChecker = require('ember-cli-version-checker'); - let checker = new VersionChecker(this); - let emberCli = checker.for('ember-cli', 'npm'); - - let requiresModulesDir = emberCli.satisfies('< 3.0.0'); - - return requiresModulesDir ? 'modules' : ''; - }, - - buildBabelOptions() { - let babelOptions = this.options.babel || {}; - let existingPlugins = babelOptions.plugins || []; - let config = this.getEmberDataConfig(); - - let customPlugins = require('./stripped-build-plugins')(config); - let plugins = existingPlugins.map((plugin) => { - return Array.isArray(plugin) ? plugin : [plugin]; - }); - plugins = plugins.concat(customPlugins.plugins); - - return { - loose: true, - plugins, - postTransformPlugins: customPlugins.postTransformPlugins, - exclude: ['transform-block-scoping', 'transform-typeof-symbol'], - }; - }, - - _setupBabelOptions() { - if (this._hasSetupBabelOptions) { - return; - } - - this.options.babel = this.buildBabelOptions(); - - this._hasSetupBabelOptions = true; - }, - - included() { - this._super.included.apply(this, arguments); - - const host = this._findHost(); - const name = this.name; - const options = host.options['@embroider/macros']?.setConfig?.[name]; - - if (options) { - Object.assign(this.options['@embroider/macros'].setOwnConfig, options); - } - - this._setupBabelOptions(); - }, - - cacheKeyForTree(treeType) { - return calculateCacheKeyForTree(treeType, this); - }, - - externalDependenciesForPrivateModule() { - return []; - }, - - treeForAddon(tree) { - if (process.env.EMBER_DATA_ROLLUP_PRIVATE === 'false' || this.shouldRollupPrivate !== true) { - return this._super.treeForAddon.call(this, tree); - } - - tree = this.debugTree(tree, 'input'); - this._setupBabelOptions(); - - let babel = this.addons.find((addon) => addon.name === 'ember-cli-babel'); - let externalDeps = this.externalDependenciesForPrivateModule(); - - const host = this._findHost(); - - // don't print this for consumers - if (this.isDevelopingAddon()) { - // eslint-disable-next-line no-console - console.log( - `Rolling up ${this.name} private modules with the following external dependencies: ['${externalDeps.join( - "', '" - )}']` - ); - } - let checker = new VersionChecker(this.project); - let emberVersion = checker.for('ember-source'); - let analyzer = this.registry.load('js').find((plugin) => plugin.name === 'ember-auto-import-analyzer'); - - let privateTree = rollupPrivateModule(tree, { - packageName: pkg.name, - babelCompiler: babel, - babelOptions: this.options.babel, - emberVersion: emberVersion, - emberCliBabelOptions: host.options && host.options['ember-cli-babel'] ? host.options['ember-cli-babel'] : {}, - onWarn: this._suppressUneededRollupWarnings.bind(this), - externalDependencies: this.externalDependenciesForPrivateModule(), - destDir: this.getOutputDirForVersion(), - analyzer, - }); - - let withoutPrivate = new Funnel(tree, { - exclude: ['-private', isProductionEnv() ? '-debug' : false].filter(Boolean), - - destDir: pkg.name, - }); - - // use the default options - let publicTree = babel.transpileTree(this.debugTree(withoutPrivate, 'babel-public:input')); - publicTree = this.debugTree(publicTree, 'babel-public:output'); - - if (analyzer) { - publicTree = analyzer.toTree.call(analyzer, publicTree, undefined, undefined, { treeType: 'addon' }); - } - - let destDir = this.getOutputDirForVersion(); - - publicTree = new Funnel(publicTree, { destDir }); - - return this.debugTree(merge([publicTree, privateTree]), 'final'); - }, - - _emberDataConfig: null, - getEmberDataConfig() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - - let options = (app.options = app.options || {}); - options.emberData = options.emberData || {}; - options.emberData.debug = options.emberData.debug || {}; - const hostOptions = options.emberData; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - options.emberData.debug - ); - options.emberData.debug = debugOptions; - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - options.emberData.includeDataAdapterInProduction = - typeof options.emberData.includeDataAdapterInProduction === 'boolean' - ? options.emberData.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE - ? isProd - ? options.emberData.includeDataAdapterInProduction - : true - : false; - options.emberData.includeDataAdapter = includeDataAdapter; - - const DEPRECATIONS = require('./deprecations')(options.emberData.compatWith || null); - const FEATURES = require('./features')(isProd); - options.emberData.__DEPRECATIONS = DEPRECATIONS; - options.emberData.__FEATURES = FEATURES; - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = options.emberData.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign( - DEPRECATIONS, - ownConfig.deprecations || {}, - hostOptions.deprecations || {} - ); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - }; -} - -module.exports = addonBuildConfigForDataPackage; diff --git a/packages/private-build-infra/src/create-version-module.js b/packages/private-build-infra/src/create-version-module.js deleted file mode 100644 index ff2f2a35490..00000000000 --- a/packages/private-build-infra/src/create-version-module.js +++ /dev/null @@ -1,40 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var createFile = require('broccoli-file-creator'); -var gitRepoInfo = require('git-repo-info'); -var npmGitInfo = require('npm-git-info'); - -function calculateVersion() { - var gitPath = path.join(__dirname, '..', '.git'); - var pkg = require('../package.json'); - var packageVersion = pkg.version; - var suffix = ''; - - var info; - if (fs.existsSync(gitPath)) { - info = gitRepoInfo(gitPath); - if (info.tag) { - return info.tag.replace(/^v/, ''); - } - - suffix = '+' + info.sha.slice(0, 10); - } else { - info = npmGitInfo(pkg); - if (info.isInstalledAsNpmPackage() && !info.hasVersionInRef()) { - suffix = '+' + info.abbreviatedSha; - } - } - - return packageVersion + suffix; -} - -module.exports = function (compatVersion) { - return createFile( - 'version.js', - 'export default "' + - calculateVersion() + - '";\n' + - (compatVersion ? `export const COMPAT_VERSION = "${compatVersion}";\n` : '') - ); -}; diff --git a/packages/private-build-infra/src/debug-macros.js b/packages/private-build-infra/src/debug-macros.js deleted file mode 100644 index 1bfa0421e5b..00000000000 --- a/packages/private-build-infra/src/debug-macros.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -module.exports = function debugMacros(config) { - const requireModule = require('./utilities/require-module'); - - const TransformPackages = require.resolve('./transforms/babel-plugin-transform-packages'); - const TransformDeprecations = require.resolve('./transforms/babel-plugin-transform-deprecations'); - const TransformDebugLogging = require.resolve('./transforms/babel-plugin-transform-logging'); - const TransformFeatures = require.resolve('./transforms/babel-plugin-transform-features'); - const TransformHasDebugPackage = require.resolve('./transforms/babel-plugin-transform-has-debug-package'); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - let plugins = [ - [ - TransformFeatures, - { - source: '@ember-data/canary-features', - flags: config.features, - }, - '@ember-data/canary-features-stripping', - ], - [ - TransformPackages, - { - source: '@ember-data/packages', - flags: MACRO_PACKAGE_FLAGS, - }, - ], - [ - TransformDeprecations, - { - source: '@ember-data/deprecations', - flags: config.deprecations, - }, - '@ember-data/deprecation-stripping', - ], - [ - TransformDebugLogging, - { - source: '@ember-data/debugging', - configKey: 'debug', - flags: config.debug, - }, - '@ember-data/debugging', - ], - [ - TransformDebugLogging, - { - source: '@ember-data/env', - configKey: 'env', - flags: { - TESTING: true, - PRODUCTION: true, - DEBUG: true, - }, - }, - '@ember-data/env', - ], - [ - TransformHasDebugPackage, - { - source: '@ember-data/packages', - flags: { HAS_DEBUG_PACKAGE: true }, - }, - '@ember-data/optional-packages-stripping', - ], - ]; - - return plugins; -}; diff --git a/packages/private-build-infra/src/debugging.js b/packages/private-build-infra/src/debugging.js deleted file mode 100644 index b8e6062c500..00000000000 --- a/packages/private-build-infra/src/debugging.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const requireModule = require('./utilities/require-module'); - -function getDebugFeatures(debugConfig, isProd) { - const { default: DEBUG_FEATURES } = requireModule('@ember-data/private-build-infra/virtual-packages/debugging.js'); - const flags = {}; - - Object.keys(DEBUG_FEATURES).forEach((flag) => { - flags[flag] = isProd ? false : debugConfig[flag] || DEBUG_FEATURES[flag]; - }); - - return flags; -} - -module.exports = getDebugFeatures; diff --git a/packages/private-build-infra/src/deprecations.js b/packages/private-build-infra/src/deprecations.js deleted file mode 100644 index 8c6bf8aa858..00000000000 --- a/packages/private-build-infra/src/deprecations.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const semver = require('semver'); - -const requireModule = require('./utilities/require-module'); - -function deprecationIsResolved(deprecatedSince, compatVersion) { - return semver.lte(semver.minVersion(deprecatedSince), semver.minVersion(compatVersion)); -} - -function getDeprecations(compatVersion) { - const { default: CURRENT_DEPRECATIONS } = requireModule( - '@ember-data/private-build-infra/virtual-packages/deprecations.js' - ); - const flags = {}; - - Object.keys(CURRENT_DEPRECATIONS).forEach((flag) => { - const deprecatedSince = CURRENT_DEPRECATIONS[flag]; - let flagState = true; // default to no code-stripping - - // if we are told we are compatible with a version - // we check if we can strip this flag - if (compatVersion) { - const isResolved = deprecationIsResolved(deprecatedSince, compatVersion); - // if we've resolved, we strip (by setting the flag to false) - /* - if (DEPRECATED_FEATURE) { - // deprecated code path - } else { - // if needed a non-deprecated code path - } - */ - flagState = !isResolved; - } - - // console.log(`${flag}=${flagState} (${deprecatedSince} <= ${compatVersion})`); - flags[flag] = flagState; - }); - - return flags; -} - -module.exports = getDeprecations; diff --git a/packages/private-build-infra/src/features.js b/packages/private-build-infra/src/features.js deleted file mode 100644 index 63bf5b159e5..00000000000 --- a/packages/private-build-infra/src/features.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const version = require('../package.json').version; - -const isCanary = version.includes('alpha'); - -const requireModule = require('./utilities/require-module'); - -function getFeatures(isProd) { - const { default: org_features } = requireModule( - '@ember-data/private-build-infra/virtual-packages/canary-features.js' - ); - const features = Object.assign({}, org_features); - - if (!isCanary) { - // disable all features with a current value of `null` - for (let feature in features) { - let featureValue = features[feature]; - - if (featureValue === null) { - features[feature] = false; - } - } - return features; - } - - const FEATURE_OVERRIDES = process.env.EMBER_DATA_FEATURE_OVERRIDE; - if (FEATURE_OVERRIDES === 'ENABLE_ALL_OPTIONAL') { - // enable all features with a current value of `null` - for (let feature in features) { - let featureValue = features[feature]; - - if (featureValue === null) { - features[feature] = true; - } - } - } else if (FEATURE_OVERRIDES === 'DISABLE_ALL') { - // disable all features, including those with a value of `true` - for (let feature in features) { - features[feature] = false; - } - } else if (FEATURE_OVERRIDES) { - // enable only the specific features listed in the environment - // variable (comma separated) - const forcedFeatures = FEATURE_OVERRIDES.split(','); - for (let i = 0; i < forcedFeatures.length; i++) { - let featureName = forcedFeatures[i]; - - features[featureName] = true; - } - } - - if (isProd) { - // disable all features with a current value of `null` - for (let feature in features) { - let featureValue = features[feature]; - - if (featureValue === null) { - features[feature] = false; - } - } - } - - return features; -} - -module.exports = getFeatures; diff --git a/packages/private-build-infra/src/packages.js b/packages/private-build-infra/src/packages.js deleted file mode 100644 index 7534df02eb5..00000000000 --- a/packages/private-build-infra/src/packages.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -const requireModule = require('./utilities/require-module'); - -function detectPackage(dep, packageName, seen) { - let isFirst = !seen; - seen = seen || new Map(); - if (seen.has(dep)) { - return false; - } - seen.set(dep, true); - - if (isFirst) { - if (dep.name() === packageName) { - return true; - } - } else if (dep.name === packageName) { - return true; - } - - if (!dep.addonPackages) { - return false; - } - - if (dep.addonPackages[packageName]) { - return true; - } - for (let i = 0; i < dep.addons.length; i++) { - if (detectPackage(dep.addons[i], packageName, seen)) { - return true; - } - } - return false; -} - -function getPackages(app) { - const { default: POSSIBLE_PACKAGES } = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const flags = {}; - const excludeDebugInProduction = - app && app.options && app.options.emberData && app.options.emberData.includeDataAdapterInProduction === false; - const isProduction = process.env.EMBER_ENV === 'production'; - - Object.keys(POSSIBLE_PACKAGES).forEach((flag) => { - const packageName = POSSIBLE_PACKAGES[flag]; - - if (packageName === '@ember-data/debug' && isProduction && excludeDebugInProduction) { - flags[flag] = false; - } else { - let hasPackage = app ? detectPackage(app.project, packageName) : true; - // console.log(`${flag}=${hasPackage}`); - flags[flag] = hasPackage; - } - }); - - return flags; -} - -module.exports = getPackages; diff --git a/packages/private-build-infra/src/stripped-build-plugins.js b/packages/private-build-infra/src/stripped-build-plugins.js deleted file mode 100644 index 8de0e3e4172..00000000000 --- a/packages/private-build-infra/src/stripped-build-plugins.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const getEnv = require('./utilities/get-env'); - -const StripClassCallCheck = require.resolve('babel6-plugin-strip-class-callcheck'); - -function isProduction(environment) { - return /production/.test(environment); -} - -module.exports = function (config = {}) { - let plugins = []; - config.env = getEnv(config); - const DebugMacros = require('./debug-macros')(config); - let postTransformPlugins = []; - - const environment = process.env.EMBER_ENV; - const isProd = isProduction(environment); - if (isProd) { - postTransformPlugins.push([StripClassCallCheck]); - } - - plugins.push(...DebugMacros); - - return { plugins, postTransformPlugins }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-convert-existence-checks-to-macros/index.js b/packages/private-build-infra/src/transforms/babel-plugin-convert-existence-checks-to-macros/index.js deleted file mode 100644 index 4a678c55956..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-convert-existence-checks-to-macros/index.js +++ /dev/null @@ -1,61 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'existence-checks', - visitor: { - ImportDeclaration(path, state) { - const replacements = state.opts.flags; - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (replacements[name]) { - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - - const exp = t.callExpression(state.importer.import(p, '@embroider/macros', 'dependencySatisfies'), [ - t.stringLiteral(replacements[name]), - t.stringLiteral('*'), - ]); - - node.replaceWith( - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', exp) : exp, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - } - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-debug-env/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-debug-env/index.js deleted file mode 100644 index 0bffaa34ca0..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-debug-env/index.js +++ /dev/null @@ -1,59 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'ast-transform', // not required - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.callExpression(state.importer.import(p, '@embroider/macros', 'isDevelopingApp'), []); - node.replaceWith( - // if (DEBUG) - // => - // if (macroCondition(isDevelopingApp()) - // t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig - // ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-deprecations/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-deprecations/index.js deleted file mode 100644 index 4ccd5555875..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-deprecations/index.js +++ /dev/null @@ -1,65 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'deprecation-flags', - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p, other) => { - let negateStatement = false; - let node = p; - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.memberExpression( - t.memberExpression( - t.callExpression(state.importer.import(p, '@embroider/macros', 'getOwnConfig'), []), - t.identifier('deprecations') - ), - t.identifier(name) - ); - node.replaceWith( - // if (DEPRECATE_FOO) - // => - // if (macroCondition(getOwnConfig().deprecations.DEPRECATE_FOO)) - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-ext.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-ext.js deleted file mode 100644 index a7371a493f8..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-ext.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = function () { - return { - name: '@ember-data/v1-addon-shim/transform-ext', - visitor: { - Program(path) { - path.node.body.forEach((node) => { - if (node.type === 'ImportDeclaration' || (node.type === 'ExportNamedDeclaration' && node.source)) { - if (node.source.value.endsWith('.js')) { - node.source.value = node.source.value.replace('.js', ''); - } - } - }); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-features/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-features/index.js deleted file mode 100644 index 0027ede52e0..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-features/index.js +++ /dev/null @@ -1,73 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -const version = require('../../../package.json').version; - -const isCanary = version.includes('alpha'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'ast-transform', // not required - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - if (!isCanary) { - p.replaceWith(t.boolean(state.opts.flags[name])); - return; - } - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.memberExpression( - t.memberExpression( - t.callExpression(state.importer.import(p, '@embroider/macros', 'getOwnConfig'), []), - t.identifier('features') - ), - t.identifier(name) - ); - node.replaceWith( - // if (LOG_FOO) - // => - // if (macroCondition(getOwnConfig().debug.LOG_FOO)) - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-has-debug-package/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-has-debug-package/index.js deleted file mode 100644 index 4abd10eb752..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-has-debug-package/index.js +++ /dev/null @@ -1,63 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'ast-transform', // not required - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.memberExpression( - t.callExpression(state.importer.import(p, '@embroider/macros', 'getOwnConfig'), []), - t.identifier('includeDataAdapter') - ); - node.replaceWith( - // if (LOG_FOO) - // => - // if (macroCondition(getOwnConfig().debug.LOG_FOO)) - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-logging/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-logging/index.js deleted file mode 100644 index 484fbb26c86..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-logging/index.js +++ /dev/null @@ -1,65 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'ast-transform', // not required - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - throw new Error(`Unexpected flag ${name} imported from ${state.opts.source}`); - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.memberExpression( - t.memberExpression( - t.callExpression(state.importer.import(p, '@embroider/macros', 'getOwnConfig'), []), - t.identifier(state.opts.configKey) - ), - t.identifier(name) - ); - node.replaceWith( - // if (LOG_FOO) - // => - // if (macroCondition(getOwnConfig().debug.LOG_FOO)) - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/transforms/babel-plugin-transform-packages/index.js b/packages/private-build-infra/src/transforms/babel-plugin-transform-packages/index.js deleted file mode 100644 index bb7da452275..00000000000 --- a/packages/private-build-infra/src/transforms/babel-plugin-transform-packages/index.js +++ /dev/null @@ -1,65 +0,0 @@ -const { ImportUtil } = require('babel-import-util'); - -function parentIsUnary(node) { - if (node.parent.type === 'UnaryExpression' && node.parent.operator === '!') { - return true; - } - return false; -} - -module.exports = function (babel) { - const { types: t } = babel; - - return { - name: 'ast-transform', // not required - visitor: { - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - - if (importPath === state.opts.source) { - const specifiers = path.get('specifiers'); - specifiers.forEach((specifier) => { - let name = specifier.node.imported.name; - if (!(name in state.opts.flags)) { - return; - } - let localBindingName = specifier.node.local.name; - let binding = specifier.scope.getBinding(localBindingName); - binding.referencePaths.forEach((p) => { - let negateStatement = false; - let node = p; - if (parentIsUnary(p)) { - negateStatement = true; - node = p.parentPath; - } - let getConfig = t.memberExpression( - t.memberExpression( - t.callExpression(state.importer.import(p, '@embroider/macros', 'getOwnConfig'), []), - t.identifier('packages') - ), - t.identifier(name) - ); - node.replaceWith( - // if (LOG_FOO) - // => - // if (macroCondition(getOwnConfig().debug.LOG_FOO)) - t.callExpression(state.importer.import(p, '@embroider/macros', 'macroCondition'), [ - negateStatement ? t.unaryExpression('!', getConfig) : getConfig, - ]) - ); - }); - specifier.scope.removeOwnBinding(localBindingName); - specifier.remove(); - }); - } - if (path.get('specifiers').length === 0) { - path.remove(); - } - }, - - Program(path, state) { - state.importer = new ImportUtil(t, path); - }, - }, - }; -}; diff --git a/packages/private-build-infra/src/utilities/detect-module.js b/packages/private-build-infra/src/utilities/detect-module.js deleted file mode 100644 index e4969635388..00000000000 --- a/packages/private-build-infra/src/utilities/detect-module.js +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable no-console */ -const path = require('node:path'); -const fs = require('node:fs'); - -const chalk = require('chalk'); - -function moduleSurelyExists(modulePath) { - try { - fs.statSync(path.join(modulePath, 'package.json')); - return true; - } catch { - return false; - } -} - -function log(str) { - if (process.env.DEBUG_MODULE_RESOLUTION) { - console.log(chalk.grey(str)); - } -} - -function bustCache(require) { - Object.keys(require.cache).forEach((key) => { - if (key.includes('ember-data')) { - delete require.cache[key]; - } - }); -} - -// do our best to detect being present -// Note: when this is not enough, consuming apps may need -// to "hoist" peer-deps or specify us as a direct dependency -// in order to deal with peer-dep bugs in package managers -module.exports = function detectModule(require, moduleName, baseDir, pkg) { - const pkgName = pkg.name; - if (moduleName === pkgName) { - return true; - } - const isDeclaredDependency = pkg.dependencies?.[moduleName] || pkg.peerDependencies?.[moduleName]; - - if (!isDeclaredDependency) { - return false; - } - - log(`\n\n${chalk.yellow(pkgName)} >> ${chalk.cyan(moduleName)} in ${chalk.white(baseDir)}`); - - const expectedLocation = path.join(baseDir, '../../', moduleName); - if (moduleSurelyExists(expectedLocation)) { - log(`\t✅ FOUND in Expected Location`); - return true; - } else { - log(`\tMISSING in ${expectedLocation}`); - } - - bustCache(require); - - try { - // try default algorithm first - require.resolve(moduleName); - log('\t✅ FOUND via normal resolution'); - return true; - } catch { - try { - bustCache(require); - // package managers have peer-deps bugs where another library - // bringing a peer-dependency doesn't necessarily result in all - // versions of the dependent getting the peer-dependency - // - // so we resolve from project as well as from our own location - require.resolve(moduleName, { paths: [baseDir, process.cwd()] }); - log('\t✅ FOUND via custom paths'); - return true; - } catch { - try { - bustCache(require); - // ember-data brings all packages so if present we are present - // - // eslint-disable-next-line node/no-missing-require - require.resolve('ember-data', { paths: [baseDir, path.join(baseDir, '../'), process.cwd()] }); - log('\t✅ FOUND ember-data'); - return true; - } catch { - log('\t🙈 NOT FOUND'); - return false; - } - } - } -}; diff --git a/packages/private-build-infra/src/utilities/edition-detector.js b/packages/private-build-infra/src/utilities/edition-detector.js deleted file mode 100644 index 5eba83070e0..00000000000 --- a/packages/private-build-infra/src/utilities/edition-detector.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const path = require('path'); - -const { has } = require('@ember/edition-utils'); - -module.exports = function (blueprint) { - blueprint.filesPath = function () { - let hasOctane = has('octane'); - if (hasOctane && process.env.EMBER_EDITION === 'classic') { - hasOctane = false; //forcible override - } - let rootPath = hasOctane ? 'native-files' : 'files'; - return path.join(blueprint.root, rootPath); - }; - - return blueprint; -}; diff --git a/packages/private-build-infra/src/utilities/extend-from-application-entity.js b/packages/private-build-infra/src/utilities/extend-from-application-entity.js deleted file mode 100644 index 352a4d77272..00000000000 --- a/packages/private-build-infra/src/utilities/extend-from-application-entity.js +++ /dev/null @@ -1,58 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const stringUtil = require('ember-cli-string-utils'); -const SilentError = require('silent-error'); -const pathUtil = require('ember-cli-path-utils'); - -module.exports = function (type, baseClass, options) { - let isAddon = options.inRepoAddon || options.project.isEmberCLIAddon(); - - let entityName = options.entity.name; - let relativePath = pathUtil.getRelativePath(options.entity.name); - - if (options.pod && options.podPath) { - relativePath = pathUtil.getRelativePath(options.podPath + options.entity.name); - } - - let applicationEntityPath = path.join(options.project.root, 'app', `${type}s`, 'application.js'); - - let hasApplicationEntity = fs.existsSync(applicationEntityPath); - if (!isAddon && !options.baseClass && entityName !== 'application' && hasApplicationEntity) { - options.baseClass = 'application'; - } - - if (options.baseClass === entityName) { - throw new SilentError( - stringUtil.classify(type) + - 's cannot extend from themself. To resolve this, remove the `--base-class` option or change to a different base-class.' - ); - } - - let importStatement; - - if (options.baseClass) { - let baseClassPath = options.baseClass; - baseClass = stringUtil.classify(baseClassPath.replace('/', '-')); - baseClass = baseClass + stringUtil.classify(type); - - importStatement = `import ${baseClass} from '${relativePath}${baseClassPath}';`; - } else { - let baseClassPath = `@ember-data/${type}`; - - if (baseClass.startsWith('JSONAPI')) { - baseClassPath += '/json-api'; - } - - if (baseClass.startsWith('REST')) { - baseClassPath += '/rest'; - } - - importStatement = `import ${baseClass} from '${baseClassPath}';`; - } - - return { - importStatement, - baseClass, - }; -}; diff --git a/packages/private-build-infra/src/utilities/get-env.js b/packages/private-build-infra/src/utilities/get-env.js deleted file mode 100644 index bacc48e7c35..00000000000 --- a/packages/private-build-infra/src/utilities/get-env.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = function getEnv() { - const { EMBER_ENV, IS_TESTING, EMBER_CLI_TEST_COMMAND } = process.env; - const PRODUCTION = EMBER_ENV === 'production'; - const DEBUG = !PRODUCTION; - const TESTING = DEBUG || Boolean(EMBER_ENV === 'test' || IS_TESTING || EMBER_CLI_TEST_COMMAND); - - return { - TESTING, - PRODUCTION, - DEBUG, - }; -}; diff --git a/packages/private-build-infra/src/utilities/module-prefix-for-project.js b/packages/private-build-infra/src/utilities/module-prefix-for-project.js deleted file mode 100644 index bd051380526..00000000000 --- a/packages/private-build-infra/src/utilities/module-prefix-for-project.js +++ /dev/null @@ -1,5 +0,0 @@ -const { dasherize } = require('ember-cli-string-utils'); - -module.exports = function modulePrefixForProject(project) { - return dasherize(project.config().modulePrefix); -}; diff --git a/packages/private-build-infra/src/utilities/require-module.js b/packages/private-build-infra/src/utilities/require-module.js deleted file mode 100644 index 295fb36beb3..00000000000 --- a/packages/private-build-infra/src/utilities/require-module.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const PGK_ROOT = path.join(__dirname, '../../'); - -module.exports = function requireModule(modulePath) { - if (modulePath.startsWith('@ember-data/private-build-infra/')) { - modulePath = modulePath.replace('@ember-data/private-build-infra/', PGK_ROOT); - } else if (modulePath.startsWith('@ember-data/private-build-infra')) { - modulePath = modulePath.replace('@ember-data/private-build-infra', PGK_ROOT); - } - const path = require.resolve(modulePath); - const fileContents = fs.readFileSync(path, { encoding: 'utf8' }); - let newContents; - - if (fileContents.includes('export default')) { - newContents = fileContents.replace('export default ', 'return '); - } else { - newContents = replaceAll(fileContents, 'export const ', 'module.exports.'); - newContents = `const module = { exports: {} };\n${newContents}\nreturn module.exports;`; - } - try { - const func = new Function(newContents); - return { default: func() }; - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - } -}; - -function replaceAll(str, pattern, replacement) { - if (str.replaceAll) { - return str.replaceAll(pattern, replacement); - } - return str.replace(new RegExp(pattern, 'g'), replacement); -} diff --git a/packages/private-build-infra/src/utilities/rollup-private-module.js b/packages/private-build-infra/src/utilities/rollup-private-module.js deleted file mode 100644 index 914b64b92b7..00000000000 --- a/packages/private-build-infra/src/utilities/rollup-private-module.js +++ /dev/null @@ -1,94 +0,0 @@ -const Funnel = require('broccoli-funnel'); -const Rollup = require('broccoli-rollup'); -const BroccoliDebug = require('broccoli-debug'); - -module.exports = function rollupPrivateModule(tree, options) { - const { onWarn, destDir, babelCompiler, babelOptions, externalDependencies, packageName, analyzer } = options; - const debugTree = BroccoliDebug.buildDebugCallback(`ember-data:${packageName}:rollup-private`); - tree = debugTree(tree, 'input'); - - let withPrivate = new Funnel(tree, { - srcDir: '-private', - destDir: '-private', - }); - - const emberCliBabelOptions = { - // we leave our output as valid ES - // for the consuming app's config to transpile as desired - // so we don't want to compileModules to amd here - compileModules: false, - - // we never need this on our own stuff - disableEmberDataPackagesPolyfill: true, - - // TODO for the embroider world we want to leave our -private module - // as an es module and only transpile the few things we genuinely care about. - // ideally this would occur as a pre-publish step so that consuming apps would - // just see a `-private.js` file and not pay any additional costs. - // CURRENTLY we transpile the -private module fully acccording to the - // consuming app's config, so we must leave these enabled. - disablePresetEnv: false, - disableDebugTooling: false, - disableDecoratorTransforms: false, - enableTypeScriptTransform: true, - - throwUnlessParallelizable: true, - - // consuming app will take care of this if needed, - // we don't need to also include - includePolyfill: false, - - // defer to consuming app's selection - // necessary as only consuming app can set this, must only have - // one copy - includeExternalHelpers: options.emberCliBabelOptions.includeExternalHelpers || false, - - extensions: ['js', 'ts'], - }; - - // and we don't want - // to convert imports to globals when real modules is possible - // this is necessary because compileModules: false forces globals - // conversion without it. - if (options.emberVersion.gte('3.27.0')) { - // TODO should we just set this all the time? - // yes, this needs to be "false" to disable it in 3.27+ - // when compileModules is false (which it is) - emberCliBabelOptions.disableEmberModulesAPIPolyfill = false; - } - - let privateTree = babelCompiler.transpileTree(debugTree(withPrivate, 'babel-private:input'), { - babel: babelOptions, - 'ember-cli-babel': emberCliBabelOptions, - }); - - if (analyzer) { - privateTree = analyzer.toTree.call(analyzer, privateTree, undefined, undefined, { treeType: 'addon' }); - } - - privateTree = debugTree(privateTree, 'babel-private:output'); - privateTree = new Rollup(privateTree, { - rollup: { - input: '-private/index.js', - output: [ - { - file: `${packageName}/-private.js`, - format: options.babelCompiler.shouldCompileModules() ? 'amd' : 'esm', - amd: { id: `${packageName}/-private` }, - exports: 'named', - generatedCode: 'es2015', - minifyInternalExports: true, - }, - ], - treeshake: true, - external: externalDependencies, - onwarn: onWarn, - // cache: true|false Defaults to true - }, - }); - - privateTree = debugTree(privateTree, 'rollup-output'); - privateTree = new Funnel(privateTree, { destDir }); - - return privateTree; -}; diff --git a/packages/private-build-infra/src/utilities/test-framework-detector.js b/packages/private-build-infra/src/utilities/test-framework-detector.js deleted file mode 100644 index e6bcae375a8..00000000000 --- a/packages/private-build-infra/src/utilities/test-framework-detector.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const VersionChecker = require('ember-cli-version-checker'); - -module.exports = function (blueprint) { - blueprint.supportsAddon = function () { - return false; - }; - - blueprint.filesPath = function () { - let type; - - let dependencies = this.project.dependencies(); - - if ('ember-qunit' in dependencies) { - if (fs.existsSync(blueprint.root + '/qunit-rfc-232-files')) { - type = 'qunit-rfc-232'; - } else { - type = 'qunit'; - } - } else if ('ember-cli-qunit' in dependencies) { - let checker = new VersionChecker(this.project); - if ( - fs.existsSync(blueprint.root + '/qunit-rfc-232-files') && - checker.for('ember-cli-qunit', 'npm').gte('4.2.0') - ) { - type = 'qunit-rfc-232'; - } else { - type = 'qunit'; - } - } else if ('ember-mocha' in dependencies) { - let checker = new VersionChecker(this.project); - if (fs.existsSync(blueprint.root + '/mocha-rfc-232-files') && checker.for('ember-mocha', 'npm').gte('0.14.0')) { - type = 'mocha-rfc-232'; - } else { - type = 'mocha'; - } - } else if ('ember-cli-mocha' in dependencies) { - type = 'mocha'; - } else { - this.ui.writeLine("Couldn't determine test style - using QUnit"); - type = 'qunit'; - } - - return path.join(blueprint.root, type + '-files'); - }; - - return blueprint; -}; diff --git a/packages/private-build-infra/src/v2-babel-build-pack.js b/packages/private-build-infra/src/v2-babel-build-pack.js deleted file mode 100644 index 6641d934dc9..00000000000 --- a/packages/private-build-infra/src/v2-babel-build-pack.js +++ /dev/null @@ -1,37 +0,0 @@ -const pkg = require('../package.json'); - -const getEnv = require('./utilities/get-env'); -// eslint-disable-next-line import/order -const requireModule = require('./utilities/require-module'); - -const debugFlags = requireModule('@ember-data/private-build-infra/virtual-packages/debugging.js'); -const deprecationFlags = requireModule('@ember-data/private-build-infra/virtual-packages/deprecations.js'); -const featureFlags = requireModule('@ember-data/private-build-infra/virtual-packages/canary-features.js'); - -const isCanary = pkg.version.includes('alpha'); - -const features = {}; -Object.keys(featureFlags).forEach((flag) => { - if (isCanary) { - features[flag] = featureFlags[flag]; - } else { - const value = featureFlags[flag]; - - if (value === null) { - features[flag] = false; - } else { - features[flag] = value; - } - } -}); - -const config = { - debug: Object.assign({}, debugFlags.default), - deprecations: Object.assign({}, deprecationFlags.default), - features, - env: getEnv(), -}; - -const plugins = require('./debug-macros')(config); - -module.exports = plugins; diff --git a/packages/private-build-infra/virtual-packages/canary-features.d.ts b/packages/private-build-infra/virtual-packages/canary-features.d.ts deleted file mode 100644 index ed43316344a..00000000000 --- a/packages/private-build-infra/virtual-packages/canary-features.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const SAMPLE_FEATURE_FLAG: boolean | null; diff --git a/packages/private-build-infra/virtual-packages/canary-features.js b/packages/private-build-infra/virtual-packages/canary-features.js deleted file mode 100644 index 2bc95033d67..00000000000 --- a/packages/private-build-infra/virtual-packages/canary-features.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * ## Canary Features - * - * EmberData allows users to test features that are implemented but not yet - * available even in canary. - * - * Typically these features represent work that might introduce a new concept, - * new API, change an API, or risk an unintended change in behavior to consuming - * applications. - * - * Such features have their implementations guarded by a "feature flag", and the - * flag is only activated once the core-data team is prepared to ship the work - * in a canary release. - * - * ### Installing Canary - * - * To test a feature you MUST be using a canary build. Canary builds are published - * to `npm` and can be installed using a precise tag (such as `ember-data@3.16.0-alpha.1`) - * or by installing the latest dist-tag published to the `canary` channel using your javascript - * package manager of choice. For instance with [pnpm](https://pnpm.io/) - - ```cli - pnpm add ember-data@canary - ``` - * - * ### Activating a Canary Feature - * - * Once you have installed canary, feature-flags can be activated at build-time - * - * by setting an environment variable: - * - * ```cli - * # Activate a single flag - * EMBER_DATA_FEATURE_OVERRIDE=SOME_FLAG ember build - * - * # Activate multiple flags by separating with commas - * EMBER_DATA_FEATURE_OVERRIDE=SOME_FLAG,OTHER_FLAG ember build - * - * # Activate all flags - * EMBER_DATA_FEATURE_OVERRIDE=ENABLE_ALL_OPTIONAL ember build - * ``` - * - * or by setting the appropriate flag in your `ember-cli-build` file: - * - * ```ts - * let app = new EmberApp(defaults, { - * emberData: { - * features: { - * SAMPLE_FEATURE_FLAG: false // utliize existing behavior, strip code for the new feature - * OTHER_FEATURE_FLAG: true // utilize this new feature, strip code for the older behavior - * } - * } - * }) - * ``` - * - * **The "off" branch of feature-flagged code is always stripped from production builds.** - * - * The list of available feature-flags is located [here](https://github.com/emberjs/data/tree/main/packages/private-build-infra/virtual-packages/canary-features.js "List of EmberData FeatureFlags") - * - * - * ### Preparing a Project to use a Canary Feature - * - * For most projects, simple version detection should be enough. - * Using the provided version compatibility helpers from [embroider-macros](https://github.com/embroider-build/embroider/tree/main/packages/macros#readme) - * the following can be done: - * - * ```js - * if (macroCondition(dependencySatisfies('@ember-data/store', '5.0'))) { - * // do thing - * } - * ``` - * - @module @ember-data/canary-features - @main @ember-data/canary-features - */ -/** - This is the current list of features used at build time for canary releases. - If empty there are no features currently gated by feature flags. - - The valid values are: - - - `true` | The feature is **enabled** at all times, and cannot be disabled. - - `false` | The feature is **disabled** at all times, and cannot be enabled. - - `null` | The feature is **disabled by default**, but can be enabled via configuration. - - @class CanaryFeatureFlags - @public -*/ -export const SAMPLE_FEATURE_FLAG = null; diff --git a/packages/private-build-infra/virtual-packages/debugging.d.ts b/packages/private-build-infra/virtual-packages/debugging.d.ts deleted file mode 100644 index f1eeebe3855..00000000000 --- a/packages/private-build-infra/virtual-packages/debugging.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const LOG_PAYLOADS: boolean; -export const LOG_OPERATIONS: boolean; -export const LOG_MUTATIONS: boolean; -export const LOG_NOTIFICATIONS: boolean; -export const LOG_REQUESTS: boolean; -export const LOG_REQUEST_STATUS: boolean; -export const LOG_IDENTIFIERS: boolean; -export const LOG_GRAPH: boolean; -export const LOG_INSTANCE_CACHE: boolean; diff --git a/packages/private-build-infra/virtual-packages/debugging.js b/packages/private-build-infra/virtual-packages/debugging.js deleted file mode 100644 index b9d54689d59..00000000000 --- a/packages/private-build-infra/virtual-packages/debugging.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - @module @ember-data/debug - */ -/** - * - * Many portions of the internals are helpfully instrumented with logging that can be activated -at build time. This instrumentation is always removed from production builds or any builds -that has not explicitly activated it. To activate it set the appropriate flag to `true`. - -```ts - let app = new EmberApp(defaults, { - emberData: { - debug: { - LOG_PAYLOADS: false, // data store received to update cache with - LOG_OPERATIONS: false, // updates to cache remote state - LOG_MUTATIONS: false, // updates to cache local state - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - } - } - }); - ``` - - @class DebugLogging - @public - */ -/** - * log payloads received by the store - * via `push` or returned from a delete/update/create - * operation. - * - * @property {boolean} LOG_PAYLOADS - * @public - */ -export const LOG_PAYLOADS = false; -/** - * log remote-state updates to the cache - * - * @property {boolean} LOG_OPERATIONS - * @public - */ -export const LOG_OPERATIONS = false; -/** - * log local-state updates to the cache - * - * @property {boolean} LOG_MUTATIONS - * @public - */ -export const LOG_MUTATIONS = false; -/** - * log notifications received by the NotificationManager - * - * @property {boolean} LOG_NOTIFICATIONS - * @public - */ -export const LOG_NOTIFICATIONS = false; -/** - * log requests issued by the RequestManager - * - * @property {boolean} LOG_REQUESTS - * @public - */ -export const LOG_REQUESTS = false; -/** - * log updates to requests the store has issued to - * the network (adapter) to fulfill. - * - * @property {boolean} LOG_REQUEST_STATUS - * @public - */ -export const LOG_REQUEST_STATUS = false; -/** - * log peek, generation and updates to - * Record Identifiers. - * - * @property {boolean} LOG_IDENTIFIERS - * @public - */ -export const LOG_IDENTIFIERS = false; -/** - * log updates received by the graph (relationship pointer storage) - * - * @property {boolean} LOG_GRAPH - * @public - */ -export const LOG_GRAPH = false; -/** - * log creation/removal of RecordData and Record - * instances. - * - * @property {boolean} LOG_INSTANCE_CACHE - * @public - */ -export const LOG_INSTANCE_CACHE = false; diff --git a/packages/private-build-infra/virtual-packages/deprecations.d.ts b/packages/private-build-infra/virtual-packages/deprecations.d.ts deleted file mode 100644 index e5b7b1fa760..00000000000 --- a/packages/private-build-infra/virtual-packages/deprecations.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// deprecations -export const DEPRECATE_CATCH_ALL: boolean; -export const DEPRECATE_3_12: boolean; -export const DEPRECATE_SAVE_PROMISE_ACCESS: boolean; -export const DEPRECATE_RSVP_PROMISE: boolean; -export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS: boolean; -export const DEPRECATE_STORE_FIND: boolean; -export const DEPRECATE_HAS_RECORD: boolean; -export const DEPRECATE_STRING_ARG_SCHEMAS: boolean; -export const DEPRECATE_JSON_API_FALLBACK: boolean; -export const DEPRECATE_MODEL_REOPEN: boolean; -export const DEPRECATE_EARLY_STATIC: boolean; -export const DEPRECATE_HELPERS: boolean; -export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS: boolean; -export const DEPRECATE_V1CACHE_STORE_APIS: boolean; -export const DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE: boolean; -export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC: boolean; -export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE: boolean; -export const DEPRECATE_V1_RECORD_DATA: boolean; -export const DEPRECATE_A_USAGE: boolean; -export const DEPRECATE_PROMISE_PROXIES: boolean; -export const DEPRECATE_ARRAY_LIKE: boolean; -export const DEPRECATE_COMPUTED_CHAINS: boolean; -export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM: boolean; -export const DEPRECATE_INSTANTIATE_RECORD_ARGS: boolean; -export const DEPRECATE_MANY_ARRAY_DUPLICATES_4_12: boolean; diff --git a/packages/private-build-infra/virtual-packages/deprecations.js b/packages/private-build-infra/virtual-packages/deprecations.js deleted file mode 100644 index c985575305e..00000000000 --- a/packages/private-build-infra/virtual-packages/deprecations.js +++ /dev/null @@ -1,761 +0,0 @@ -/** - * ## Deprecations - * - * EmberData allows users to opt-in and remove code that exists to support deprecated - * behaviors. - * - * If your app has resolved all deprecations present in a given version, - * you may specify that version as your "compatibility" version to remove - * the code that supported the deprecated behavior from your app. - * - * For instance, if a deprecation was introduced in 3.13, and the app specifies - * 3.13 as its minimum version compatibility, any deprecations introduced before - * or during 3.13 would be stripped away. - * - * An app can use a different version than what it specifies as it's compatibility - * version. For instance, an App could be using `3.16` while specifying compatibility - * with `3.12`. This would remove any deprecations that were present in or before `3.12` - * but keep support for anything deprecated in or above `3.13`. - * - * ### Configuring Compatibility - * - * To configure your compatibility version, set the `compatWith` to the version you - * are compatible with on the `emberData` config in your `ember-cli-build.js` file. - * - * ```js - * let app = new EmberApp(defaults, { - * emberData: { - * compatWith: '3.12', - * }, - * }); - * ``` - * - * Alternatively, individual deprecations can be resolved (and thus have its support stripped) - * via one of the flag names listed below. For instance, given a flag named `DEPRECATE_FOO_BEHAVIOR`. - * - * This capability is interopable with `compatWith`. You may set `compatWith` and then selectively resolve - * additional deprecations, or set compatWith and selectively un-resolve specific deprecations. - * - * Note: EmberData does not test against permutations of deprecations being stripped, our tests run against - * "all deprecated code included" and "all deprecated code removed". Unspecified behavior may sometimes occur - * when removing code for only some deprecations associated to a version number. - * - * ```js - * let app = new EmberApp(defaults, { - * emberData: { - * deprecations: { - * DEPRECATE_FOO_BEHAVIOR: false // set to false to strip this code - * DEPRECATE_BAR_BEHAVIOR: true // force to true to not strip this code - * } - * } - * }) - * ``` - * - * The complete list of which versions specific deprecations will be removed in - * can be found [here](https://github.com/emberjs/data/blob/main/packages/private-build-infra/virtual-packages/deprecations.js "List of EmberData Deprecations") - * - * @module @ember-data/deprecations - * @main @ember-data/deprecations - */ - -/** - * The following list represents deprecations currently active. - * - * Some deprecation flags guard multiple deprecation IDs. All - * associated IDs are listed. - * - * @class CurrentDeprecations - * @public - */ -export const DEPRECATE_CATCH_ALL = '99.0'; -export const DEPRECATE_3_12 = '3.12'; - -/** - * **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-v1cache-store-apis** - * - * Deprecates various methods on the store and store-cache-wrapper - * that were specific to the v1 cache. - * - * Most applications should not encounter this deprecation, but if you - * do it means that an addon you are using is likely using these methods - * as part of having implemented its own cache. - * - * The implementation will need to update to the V2 Cache API equivalent method - * as detailed in the deprecation method. Generally this means the implementation - * needs to be more broadly reworked to use the newer V2.1 Cache API. - * - * @property DEPRECATE_V1CACHE_STORE_APIS - * @since 4.7 - * @until 5.0 - * @public - */ -export const DEPRECATE_V1CACHE_STORE_APIS = '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:deprecate-v1-cache** - * - * Deprecates instantiating a non-singleton cache via `store.createRecordDataFor` - * in favor of a singleton-cache via `store.createCache`. - * - * Most applications should not encounter this deprecation, but if you - * do it means that an addon you are using is likely using an unsupported cache - * implementation. - * - * The implementation will need to update to the V2 Cache API and be integrated - * via the `createCache` hook. - * - * @property DEPRECATE_V1_RECORD_DATA - * @since 4.12 - * @until 5.0 - * @public - */ -export const DEPRECATE_V1_RECORD_DATA = '4.12'; - -/** - * **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-instantiate-record-args** - * - * Deprecates using the former 3rd and 4th arguments to `Store.instantiateRecord` which are now - * available as properties on the store. - * - * **old** - * ```ts - * { - * instantiateRecord(identifier, createArgs, recordDataFor, notifications) { - * const cache = recordDataFor(identifier); - * } - * } - * ``` - * - * **new** - * ```ts - * { - * instantiateRecord(identifier, createArgs) { - * const { cache, notifications } = this; - * } - * } - * ``` - * - * @property DEPRECATE_INSTANTIATE_RECORD_ARGS - * @since 4.7 - * @until 5.0 - * @public - */ -export const DEPRECATE_INSTANTIATE_RECORD_ARGS = '4.12'; - -/** - * **id: ember-data:deprecate-many-array-duplicates-4-12** - * - * HACK: This deprecation flag is being used as a feature flag to optionally - * disable adding duplicate records to a `ManyArray`. - * - * When the flag is `true` (default), duplicate records will be de-duped. - * - * When the flag is `false`, an error will be thrown when duplicates are added. - * NOTE: this is not a deprecation error! - * - * In 5.3, we expect to add a corollary deprecation - * `DEPRECATE_MANY_ARRAY_DUPLICATES` that will actually `deprecate` when `true`. - * - * @property DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 - * @since 4.12 - * @until 6.0 - * @public - */ -export const DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 = '4.12'; diff --git a/packages/private-build-infra/virtual-packages/env.d.ts b/packages/private-build-infra/virtual-packages/env.d.ts deleted file mode 100644 index ee75ffb830c..00000000000 --- a/packages/private-build-infra/virtual-packages/env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DEBUG: boolean; -export const TESTING: boolean; -export const PRODUCTION: boolean; diff --git a/packages/private-build-infra/virtual-packages/packages.d.ts b/packages/private-build-infra/virtual-packages/packages.d.ts deleted file mode 100644 index 476beea3689..00000000000 --- a/packages/private-build-infra/virtual-packages/packages.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const HAS_EMBER_DATA_PACKAGE: boolean; -export const HAS_STORE_PACKAGE: boolean; -export const HAS_MODEL_PACKAGE: boolean; -export const HAS_ADAPTER_PACKAGE: boolean; -export const HAS_SERIALIZER_PACKAGE: boolean; -export const HAS_DEBUG_PACKAGE: boolean; -export const HAS_JSON_API_PACKAGE: boolean; -export const HAS_GRAPH_PACKAGE: boolean; -export const HAS_REQUEST_PACKAGE: boolean; -export const HAS_TRACKING_PACKAGE: boolean; -export const HAS_COMPAT_PACKAGE: boolean; diff --git a/packages/private-build-infra/virtual-packages/packages.js b/packages/private-build-infra/virtual-packages/packages.js deleted file mode 100644 index c75784c6f37..00000000000 --- a/packages/private-build-infra/virtual-packages/packages.js +++ /dev/null @@ -1,11 +0,0 @@ -export const HAS_EMBER_DATA_PACKAGE = 'ember-data'; -export const HAS_STORE_PACKAGE = '@ember-data/store'; -export const HAS_MODEL_PACKAGE = '@ember-data/model'; -export const HAS_JSON_API_PACKAGE = '@ember-data/json-api'; -export const HAS_GRAPH_PACKAGE = '@ember-data/graph'; -export const HAS_REQUEST_PACKAGE = '@ember-data/request'; -export const HAS_COMPAT_PACKAGE = '@ember-data/legacy-compat'; -export const HAS_TRACKING_PACKAGE = '@ember-data/tracking'; -export const HAS_ADAPTER_PACKAGE = '@ember-data/adapter'; -export const HAS_SERIALIZER_PACKAGE = '@ember-data/serializer'; -export const HAS_DEBUG_PACKAGE = '@ember-data/debug'; diff --git a/packages/request-utils/CHANGELOG.md b/packages/request-utils/CHANGELOG.md new file mode 100644 index 00000000000..3923f847edf --- /dev/null +++ b/packages/request-utils/CHANGELOG.md @@ -0,0 +1,67 @@ +# @ember-data/request-utils 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)) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9314](https://github.com/emberjs/data/pull/9314) feat: improve lifetime handling of ad-hoc createRecord requests ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@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)) +* [#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)) + +#### :bug: Bug Fix + +* [#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)) + +#### :house: Internal + +* [#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)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/request-utils/addon-main.cjs b/packages/request-utils/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/request-utils/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/request-utils/addon-main.js b/packages/request-utils/addon-main.js deleted file mode 100644 index 459ef9174ca..00000000000 --- a/packages/request-utils/addon-main.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - name: require('./package.json').name, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/request-utils/babel.config.json b/packages/request-utils/babel.config.json deleted file mode 100644 index 0e04314a08c..00000000000 --- a/packages/request-utils/babel.config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "plugins": [ - "@babel/plugin-transform-runtime", - ["@babel/plugin-transform-typescript", { "allowDeclareFields": true }], - ["@babel/plugin-proposal-decorators", { "legacy": true }], - "@babel/plugin-transform-class-properties" - ] -} diff --git a/packages/request-utils/babel.config.mjs b/packages/request-utils/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/request-utils/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/request-utils/eslint.config.mjs b/packages/request-utils/eslint.config.mjs new file mode 100644 index 00000000000..63296bac802 --- /dev/null +++ b/packages/request-utils/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: ['@ember/debug'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/request-utils/package.json b/packages/request-utils/package.json index dcfb2cb0500..eb27c28be46 100644 --- a/packages/request-utils/package.json +++ b/packages/request-utils/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/emberjs/data", "bugs": "https://github.com/emberjs/data/issues", "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "keywords": [ "ember-addon" @@ -21,12 +21,10 @@ "volta": { "extends": "../../package.json" }, - "dependencies": { - "ember-cli-babel": "^8.2.0" - }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", @@ -34,43 +32,67 @@ ], "exports": { ".": { - "default": "./addon/index.js" + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" } }, "scripts": { - "build:client": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "_build": "bun run build:client", - "prepack": "bun run _build", - "_syncPnpm": "bun run sync-dependencies-meta-injected" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 + }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember/string": "^3.1.1 || ^4.0.0", + "@warp-drive/core-types": "workspace:*", + "ember-inflector": "^4.0.2 || ^5.0.0" + }, + "peerDependenciesMeta": { + "ember-inflector": { + "optional": true + }, + "@ember/string": { + "optional": true + } + }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/cli": "^7.24.1", - "@babel/core": "^7.24.4", - "@babel/plugin-proposal-decorators": "^7.24.1", - "@babel/plugin-transform-class-properties": "^7.24.1", - "@babel/plugin-transform-runtime": "^7.24.3", - "@babel/plugin-transform-typescript": "^7.24.4", - "@babel/preset-env": "^7.24.4", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", "@babel/preset-typescript": "^7.24.1", - "@babel/runtime": "^7.24.4", - "@embroider/addon-dev": "^4.3.1", "@glimmer/component": "^1.1.2", - "@rollup/plugin-babel": "^6.0.4", - "@rollup/plugin-node-resolve": "^15.2.3", - "ember-source": "~5.7.0", - "pnpm-sync-dependencies-meta-injected": "0.0.10", - "rollup": "^4.14.3", - "rsvp": "^4.8.5", + "@ember/string": "3.1.1", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "ember-inflector": "4.0.3", + "pnpm-sync-dependencies-meta-injected": "0.0.14", "typescript": "^5.4.5", - "walk-sync": "^3.0.0", - "webpack": "^5.77.0" + "vite": "^5.2.11" }, "ember": { "edition": "octane" + }, + "dependenciesMeta": { + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + } } } diff --git a/packages/request-utils/rollup.config.mjs b/packages/request-utils/rollup.config.mjs deleted file mode 100644 index 5b71e0ab162..00000000000 --- a/packages/request-utils/rollup.config.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -import { external } from './rollup/external.cjs'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: external(['@ember/debug']), - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', '-private.js']), - - nodeResolve({ extensions: ['.ts'] }), - babel({ - extensions: ['.ts'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/request-utils/rollup/external.cjs b/packages/request-utils/rollup/external.cjs deleted file mode 100644 index a8f1e8ea947..00000000000 --- a/packages/request-utils/rollup/external.cjs +++ /dev/null @@ -1,37 +0,0 @@ -const path = require('path'); - -function external(manual = []) { - const pkg = require(path.join(process.cwd(), './package.json')); - const deps = Object.keys(pkg.dependencies || {}); - const peers = Object.keys(pkg.peerDependencies || {}); - const all = new Set([...deps, ...peers, ...manual]); - - // console.log({ externals: result }); - return function (id) { - if (all.has(id)) { - return true; - } - - for (const dep of deps) { - if (id.startsWith(dep + '/')) { - return true; - } - } - - for (const dep of peers) { - if (id.startsWith(dep + '/')) { - return true; - } - } - - if (id.startsWith('@ember/') || id.startsWith('@ember-data/') || id.startsWith('@warp-drive/')) { - throw new Error(`Unexpected import: ${id}`); - } - - return false; - }; -} - -module.exports = { - external, -}; diff --git a/packages/request-utils/src/-private/string/inflect.ts b/packages/request-utils/src/-private/string/inflect.ts new file mode 100644 index 00000000000..af62224e686 --- /dev/null +++ b/packages/request-utils/src/-private/string/inflect.ts @@ -0,0 +1,169 @@ +import { assert } from '@warp-drive/build-config/macros'; + +import { defaultRules } from './inflections'; +import { capitalize, LRUCache } from './transform'; + +const BLANK_REGEX = /^\s*$/; +const LAST_WORD_DASHED_REGEX = /([\w/-]+[_/\s-])([a-z\d]+$)/; +const LAST_WORD_CAMELIZED_REGEX = /([\w/\s-]+)([A-Z][a-z\d]*$)/; +const CAMELIZED_REGEX = /[A-Z][a-z\d]*$/; + +const SINGULARS = new LRUCache((word: string) => { + return _singularize(word); +}); +const PLURALS = new LRUCache((word: string) => { + return _pluralize(word); +}); +const UNCOUNTABLE = new Set(defaultRules.uncountable); +const IRREGULAR: Map = new Map(); +const INVERSE_IRREGULAR: Map = new Map(); +const SINGULAR_RULES = new Map(defaultRules.singular.reverse()); +const PLURAL_RULES = new Map(defaultRules.plurals.reverse()); + +export function uncountable(word: string) { + UNCOUNTABLE.add(word.toLowerCase()); +} + +export function loadUncountable(uncountables: string[]) { + uncountables.forEach((word) => { + uncountable(word); + }); +} + +export function irregular(single: string, plur: string) { + //pluralizing + IRREGULAR.set(single.toLowerCase(), plur); + IRREGULAR.set(plur.toLowerCase(), plur); + + //singularizing + INVERSE_IRREGULAR.set(plur.toLowerCase(), single); + INVERSE_IRREGULAR.set(single.toLowerCase(), single); +} + +export function loadIrregular(irregularPairs: Array<[string, string]>) { + irregularPairs.forEach((pair) => { + //pluralizing + IRREGULAR.set(pair[0].toLowerCase(), pair[1]); + IRREGULAR.set(pair[1].toLowerCase(), pair[1]); + + //singularizing + INVERSE_IRREGULAR.set(pair[1].toLowerCase(), pair[0]); + INVERSE_IRREGULAR.set(pair[0].toLowerCase(), pair[0]); + }); +} +loadIrregular(defaultRules.irregularPairs); + +export function clear() { + SINGULARS.clear(); + PLURALS.clear(); +} + +export function resetToDefaults() { + clearRules(); + defaultRules.uncountable.forEach((v) => UNCOUNTABLE.add(v)); + defaultRules.singular.forEach((v) => SINGULAR_RULES.set(v[0], v[1])); + defaultRules.plurals.forEach((v) => PLURAL_RULES.set(v[0], v[1])); + loadIrregular(defaultRules.irregularPairs); +} + +export function clearRules() { + SINGULARS.clear(); + PLURALS.clear(); + UNCOUNTABLE.clear(); + IRREGULAR.clear(); + INVERSE_IRREGULAR.clear(); + SINGULAR_RULES.clear(); + PLURAL_RULES.clear(); +} + +export function singularize(word: string) { + assert(`singularize expects to receive a non-empty string`, typeof word === 'string' && word.length > 0); + if (!word) return ''; + return SINGULARS.get(word); +} + +export function pluralize(word: string) { + assert(`pluralize expects to receive a non-empty string`, typeof word === 'string' && word.length > 0); + if (!word) return ''; + return PLURALS.get(word); +} + +function unshiftMap(v: [K, V], map: Map) { + // reorder + const rules = [v, ...map.entries()]; + map.clear(); + rules.forEach((rule) => { + map.set(rule[0], rule[1]); + }); +} + +export function plural(regex: RegExp, string: string) { + // rule requires reordering if exists, so remove it first + if (PLURAL_RULES.has(regex)) { + PLURAL_RULES.delete(regex); + } + + // reorder + unshiftMap([regex, string], PLURAL_RULES); +} + +export function singular(regex: RegExp, string: string) { + // rule requires reordering if exists, so remove it first + if (SINGULAR_RULES.has(regex)) { + SINGULAR_RULES.delete(regex); + } + + // reorder + unshiftMap([regex, string], SINGULAR_RULES); +} + +function _pluralize(word: string) { + return inflect(word, PLURAL_RULES, IRREGULAR); +} + +function _singularize(word: string) { + return inflect(word, SINGULAR_RULES, INVERSE_IRREGULAR); +} + +function inflect(word: string, typeRules: Map, irregulars: Map) { + // empty strings + const isBlank = !word || BLANK_REGEX.test(word); + if (isBlank) { + return word; + } + + // basic uncountables + const lowercase = word.toLowerCase(); + if (UNCOUNTABLE.has(lowercase)) { + return word; + } + + // adv uncountables + const wordSplit = LAST_WORD_DASHED_REGEX.exec(word) || LAST_WORD_CAMELIZED_REGEX.exec(word); + const lastWord = wordSplit ? wordSplit[2].toLowerCase() : null; + if (lastWord && UNCOUNTABLE.has(lastWord)) { + return word; + } + + // handle irregulars + const isCamelized = CAMELIZED_REGEX.test(word); + for (let [rule, substitution] of irregulars) { + if (lowercase.match(rule + '$')) { + if (isCamelized && lastWord && irregulars.has(lastWord)) { + substitution = capitalize(substitution); + rule = capitalize(rule); + } + + return word.replace(new RegExp(rule, 'i'), substitution); + } + } + + // do the actual inflection + for (const [rule, substitution] of typeRules) { + if (rule.test(word)) { + return word.replace(rule, substitution); + } + } + + return word; +} diff --git a/packages/request-utils/src/-private/string/inflections.ts b/packages/request-utils/src/-private/string/inflections.ts new file mode 100644 index 00000000000..6740bdd7b20 --- /dev/null +++ b/packages/request-utils/src/-private/string/inflections.ts @@ -0,0 +1,75 @@ +export type RulesArray = Array<[RegExp, string]>; +type DefaultRulesType = { + plurals: RulesArray; + singular: RulesArray; + irregularPairs: Array<[string, string]>; + uncountable: string[]; +}; + +export const defaultRules: DefaultRulesType = { + plurals: [ + [/$/, 's'], + [/s$/i, 's'], + [/^(ax|test)is$/i, '$1es'], + [/(octop|vir)us$/i, '$1i'], + [/(octop|vir)i$/i, '$1i'], + [/(alias|status|bonus)$/i, '$1es'], + [/(bu)s$/i, '$1ses'], + [/(buffal|tomat)o$/i, '$1oes'], + [/([ti])um$/i, '$1a'], + [/([ti])a$/i, '$1a'], + [/sis$/i, 'ses'], + [/(?:([^f])fe|([lr])f)$/i, '$1$2ves'], + [/(hive)$/i, '$1s'], + [/([^aeiouy]|qu)y$/i, '$1ies'], + [/(x|ch|ss|sh)$/i, '$1es'], + [/(matr|vert|ind)(?:ix|ex)$/i, '$1ices'], + [/^(m|l)ouse$/i, '$1ice'], + [/^(m|l)ice$/i, '$1ice'], + [/^(ox)$/i, '$1en'], + [/^(oxen)$/i, '$1'], + [/(quiz)$/i, '$1zes'], + ], + + singular: [ + [/s$/i, ''], + [/(ss)$/i, '$1'], + [/(n)ews$/i, '$1ews'], + [/([ti])a$/i, '$1um'], + [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '$1sis'], + [/(^analy)(sis|ses)$/i, '$1sis'], + [/([^f])ves$/i, '$1fe'], + [/(hive)s$/i, '$1'], + [/(tive)s$/i, '$1'], + [/([lr])ves$/i, '$1f'], + [/([^aeiouy]|qu)ies$/i, '$1y'], + [/(s)eries$/i, '$1eries'], + [/(m)ovies$/i, '$1ovie'], + [/(x|ch|ss|sh)es$/i, '$1'], + [/^(m|l)ice$/i, '$1ouse'], + [/(bus)(es)?$/i, '$1'], + [/(o)es$/i, '$1'], + [/(shoe)s$/i, '$1'], + [/(cris|test)(is|es)$/i, '$1is'], + [/^(a)x[ie]s$/i, '$1xis'], + [/(octop|vir)(us|i)$/i, '$1us'], + [/(alias|status|bonus)(es)?$/i, '$1'], + [/^(ox)en/i, '$1'], + [/(vert|ind)ices$/i, '$1ex'], + [/(matr)ices$/i, '$1ix'], + [/(quiz)zes$/i, '$1'], + [/(database)s$/i, '$1'], + ], + + irregularPairs: [ + ['person', 'people'], + ['man', 'men'], + ['child', 'children'], + ['sex', 'sexes'], + ['move', 'moves'], + ['cow', 'kine'], + ['zombie', 'zombies'], + ], + + uncountable: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep', 'jeans', 'police'], +}; diff --git a/packages/request-utils/src/-private/string/transform.ts b/packages/request-utils/src/-private/string/transform.ts new file mode 100644 index 00000000000..0d4dd22b21d --- /dev/null +++ b/packages/request-utils/src/-private/string/transform.ts @@ -0,0 +1,176 @@ +import { DEBUG } from '@warp-drive/build-config/env'; + +const DEFAULT_MAX_CACHE_SIZE = 10_000; +export class LRUCache { + declare size: number; + declare state: Map; + declare doWork: (k: T) => V; + + // debug stats + declare _hits: number; + declare _misses: number; + declare _ejected: number; + + constructor(doWork: (k: T) => V, size?: number) { + this.size = size || DEFAULT_MAX_CACHE_SIZE; + this.state = new Map(); + this.doWork = doWork; + + if (DEBUG) { + this._hits = 0; + this._misses = 0; + this._ejected = 0; + } + } + get(key: T) { + const value = this.state.get(key); + if (value) { + if (DEBUG) { + this._hits++; + } + this.state.delete(key); + this.state.set(key, value); + return value; + } + if (DEBUG) { + this._misses++; + } + + const newValue = this.doWork(key); + this.set(key, newValue); + return newValue; + } + + set(key: T, value: V) { + if (this.state.size === this.size) { + for (const [k] of this.state) { + if (DEBUG) { + this._ejected++; + } + this.state.delete(k); + break; + } + } + this.state.set(key, value); + } + + clear() { + this.state.clear(); + if (DEBUG) { + this._hits = 0; + this._misses = 0; + this._ejected = 0; + } + } +} + +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_DASHERIZE_CACHE = new LRUCache((key: string) => + key.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase().replace(STRING_DASHERIZE_REGEXP, '-') +); + +// eslint-disable-next-line no-useless-escape +const STRING_CAMELIZE_REGEXP_1 = /(\-|\_|\.|\s)+(.)?/g; +const STRING_CAMELIZE_REGEXP_2 = /(^|\/)([A-Z])/g; +const CAMELIZE_CACHE = new LRUCache((key: string) => + key + .replace(STRING_CAMELIZE_REGEXP_1, (_match, _separator, chr: string | null) => (chr ? chr.toUpperCase() : '')) + .replace(STRING_CAMELIZE_REGEXP_2, (match /*, separator, chr */) => match.toLowerCase()) +); + +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +// eslint-disable-next-line no-useless-escape +const STRING_UNDERSCORE_REGEXP_2 = /\-|\s+/g; +const UNDERSCORE_CACHE = new LRUCache((str: string) => + str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2').replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase() +); + +const STRING_CAPITALIZE_REGEXP = /(^|\/)([a-z\u00C0-\u024F])/g; +const CAPITALIZE_CACHE = new LRUCache((str: string) => + str.replace(STRING_CAPITALIZE_REGEXP, (match /*, separator, chr */) => match.toUpperCase()) +); + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```js + import { dasherize } from '@ember-data/request-utils/string'; + + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + dasherize('privateDocs/ownerInvoice'; // 'private-docs/owner-invoice' + ``` + + @typedoc +*/ +export function dasherize(str: string): string { + return STRING_DASHERIZE_CACHE.get(str); +} + +/** + Returns the lowerCamelCase form of a string. + + ```js + import { camelize } from '@ember-data/request-utils/string'; + + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + camelize('private-docs/owner-invoice'); // 'privateDocs/ownerInvoice' +``` + + @typedoc +*/ +export function camelize(str: string): string { + return CAMELIZE_CACHE.get(str); +} + +/** + Returns the lower\_case\_and\_underscored form of a string. + + ```js + import { underscore } from '@ember-data/request-utils/string'; + + underscore('innerHTML'); // 'inner_html' + underscore('action_name'); // 'action_name' + underscore('css-class-name'); // 'css_class_name' + underscore('my favorite items'); // 'my_favorite_items' + underscore('privateDocs/ownerInvoice'); // 'private_docs/owner_invoice' + ``` + + @typedoc +*/ +export function underscore(str: string): string { + return UNDERSCORE_CACHE.get(str); +} + +/** + Returns the Capitalized form of a string + + ```js + import { capitalize } from '@ember-data/request-utils/string'; + + capitalize('innerHTML') // 'InnerHTML' + capitalize('action_name') // 'Action_name' + capitalize('css-class-name') // 'Css-class-name' + capitalize('my favorite items') // 'My favorite items' + capitalize('privateDocs/ownerInvoice'); // 'PrivateDocs/ownerInvoice' + ``` + + @typedoc +*/ +export function capitalize(str: string): string { + return CAPITALIZE_CACHE.get(str); +} + +export function setMaxLRUCacheSize(size: number) { + CAMELIZE_CACHE.size = size; + UNDERSCORE_CACHE.size = size; + CAPITALIZE_CACHE.size = size; + STRING_DASHERIZE_CACHE.size = size; +} diff --git a/packages/request-utils/src/deprecation-support.ts b/packages/request-utils/src/deprecation-support.ts new file mode 100644 index 00000000000..d0229dc3906 --- /dev/null +++ b/packages/request-utils/src/deprecation-support.ts @@ -0,0 +1,266 @@ +import { deprecate } from '@ember/debug'; + +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; + +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'; + +if (DEPRECATE_EMBER_INFLECTOR) { + if (macroCondition(dependencySatisfies('ember-inflector', '*'))) { + const Inflector = (importSync('ember-inflector') as { default: typeof import('ember-inflector').default }).default; + const { inflector } = Inflector; + + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalPlural = inflector.plural; + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalSingular = inflector.singular; + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalIrregular = inflector.irregular; + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalUncountable = inflector.uncountable; + + // copy over any already registered rules + type DefaultRules = { + plurals: [RegExp, string][]; + singular: [RegExp, string][]; + irregularPairs: [string, string][]; + uncountable: string[]; + }; + type InternalRules = { + plurals: [RegExp, string][]; + singular: [RegExp, string][]; + + // [str1, str2] => + // { [str1.lower]: str2 } + // { [str2.lower]: str2 } + irregular: Record; + + // [str1, str2] => + // { [str2.lower]: str1 } + // { [str1.lower]: str1 } + irregularInverse: Record; + + // lower cased string + uncountable: Record; + }; + + // ember-inflector mutates the default rules arrays + // with user supplied rules, so we keep track of what + // is default via our own list. + const defaultPluralKeys = new Set(); + const defaultSingularKeys = new Set(); + WarpDriveDefaults.plurals.forEach(([regex]) => { + defaultPluralKeys.add(regex.toString()); + }); + WarpDriveDefaults.singular.forEach(([regex]) => { + defaultSingularKeys.add(regex.toString()); + }); + + const { defaultRules } = Inflector as unknown as { defaultRules: DefaultRules }; + const { rules } = inflector as unknown as { rules: InternalRules }; + + const irregularMap = new Map(); + const toIgnore = new Set(); + const uncountableSet = new Set(defaultRules.uncountable); + + defaultRules.irregularPairs.forEach(([single, plur]) => { + irregularMap.set(single.toLowerCase(), plur); + toIgnore.add(plur.toLowerCase()); + }); + const irregularLookups = new Map(); + Object.keys(rules.irregular).forEach((single) => { + const plur = rules.irregular[single]; + irregularLookups.set(single, plur); + }); + + // load plurals + rules.plurals.forEach(([regex, replacement]) => { + if (defaultPluralKeys.has(regex.toString())) { + return; + } + + plural(regex, replacement); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + }); + + // load singulars + rules.singular.forEach(([regex, replacement]) => { + if (defaultSingularKeys.has(regex.toString())) { + return; + } + + singular(regex, replacement); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + }); + + // load irregulars + Object.keys(rules.irregular).forEach((single) => { + const plur = rules.irregular[single]; + const defaultPlur = irregularMap.get(single); + if (defaultPlur && defaultPlur === plur) { + return; + } + + if (toIgnore.has(single)) { + return; + } + + const actualSingle = irregularLookups.get(plur.toLowerCase()) || single; + toIgnore.add(plur.toLowerCase()); + irregular(actualSingle, plur); + + 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}'.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + }); + + // load uncountables + Object.keys(rules.uncountable).forEach((word) => { + if (uncountableSet.has(word) || rules.uncountable[word] !== true) { + return; + } + + uncountable(word); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + }); + + inflector.plural = function (...args: Parameters) { + plural(...args); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + + return originalPlural.apply(inflector, args); + }; + + inflector.singular = function (...args: Parameters) { + singular(...args); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + + return originalSingular.apply(inflector, args); + }; + + inflector.irregular = function (...args: Parameters) { + irregular(...args); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + + return originalIrregular.apply(inflector, args); + }; + + inflector.uncountable = function (...args: Parameters) { + uncountable(...args); + + 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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'warp-drive.ember-inflector', + until: '6.0.0', + for: 'warp-drive', + since: { + enabled: '5.3.4', + available: '4.13', + }, + url: 'https://deprecations.emberjs.com/id/warp-drive.ember-inflector', + } + ); + + return originalUncountable.apply(inflector, args); + }; + } +} diff --git a/packages/request-utils/src/index.ts b/packages/request-utils/src/index.ts index ba669233bd5..6a52199e6b0 100644 --- a/packages/request-utils/src/index.ts +++ b/packages/request-utils/src/index.ts @@ -1,12 +1,47 @@ -import { assert, deprecate } from '@ember/debug'; +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'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { QueryParamsSerializationOptions, QueryParamsSource, Serializable } from '@warp-drive/core-types/params'; +import type { ImmutableRequestInfo, ResponseInfo } from '@warp-drive/core-types/request'; + +type UnsubscribeToken = object; +type CacheOperation = 'added' | 'removed' | 'updated' | 'state'; +type DocumentCacheOperation = 'invalidated' | 'added' | 'removed' | 'updated' | 'state'; + +export interface NotificationCallback { + (identifier: StableRecordIdentifier, notificationType: 'attributes' | 'relationships', key?: string): void; + (identifier: StableRecordIdentifier, notificationType: 'errors' | 'meta' | 'identity' | 'state'): void; + // (identifier: StableRecordIdentifier, notificationType: NotificationType, key?: string): void; +} + +interface ResourceOperationCallback { + // resource updates + (identifier: StableRecordIdentifier, notificationType: CacheOperation): void; +} -import type { ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import type { Cache } from '@ember-data/types/cache/cache'; -import type { ResponseInfo } from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; +interface DocumentOperationCallback { + // document updates + (identifier: StableDocumentIdentifier, notificationType: DocumentCacheOperation): void; +} + +type NotificationManager = { + subscribe(identifier: StableRecordIdentifier, callback: NotificationCallback): UnsubscribeToken; + subscribe(identifier: 'resource', callback: ResourceOperationCallback): UnsubscribeToken; + subscribe(identifier: 'document' | StableDocumentIdentifier, callback: DocumentOperationCallback): UnsubscribeToken; + + notify(identifier: StableRecordIdentifier, value: 'attributes' | 'relationships', key?: string): boolean; + notify(identifier: StableRecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'state'): boolean; + notify(identifier: StableRecordIdentifier, value: CacheOperation): boolean; + notify(identifier: StableDocumentIdentifier, value: DocumentCacheOperation): boolean; +}; type Store = { cache: Cache; + notifications: NotificationManager; }; /** @@ -53,18 +88,7 @@ type Store = { // host and namespace which are provided by the final consuming // class to the prototype which can result in overwrite errors -type SerializablePrimitive = string | number | boolean | null; -type Serializable = SerializablePrimitive | SerializablePrimitive[]; -type QueryParamsSerializationOptions = { - arrayFormat?: 'bracket' | 'indices' | 'repeat' | 'comma'; -}; -type QueryParamsSource = - | ({ - include?: string | string[]; - } & Record, Serializable>) - | URLSearchParams; - -interface BuildURLConfig { +export interface BuildURLConfig { host: string | null; namespace: string | null; } @@ -626,6 +650,7 @@ export function parseCacheControl(header: string): CacheControlValue { } if (i === header.length - 1) { + // @ts-expect-error TS incorrectly thinks that optional keys must have a type that includes undefined cacheControlValue[key] = NUMERIC_KEYS.has(key) ? parseCacheControlValue(value) : true; } } @@ -653,10 +678,10 @@ function isStale(headers: Headers, expirationTime: number): boolean { return result; } -export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires: number }; +export type PolicyConfig = { apiCacheSoftExpires: number; apiCacheHardExpires: number }; /** - * A basic LifetimesService that can be added to the Store service. + * A basic CachePolicy that can be added to the Store service. * * Determines staleness based on time since the request was last received from the API * using the `date` header. @@ -687,7 +712,7 @@ export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires * Usage: * * ```ts - * import { LifetimesService } from '@ember-data/request-utils'; + * import { CachePolicy } from '@ember-data/request-utils'; * import DataStore from '@ember-data/store'; * * // ... @@ -695,20 +720,26 @@ export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires * export class Store extends DataStore { * constructor(args) { * super(args); - * this.lifetimes = new LifetimesService({ apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 }); + * this.lifetimes = new CachePolicy({ apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 }); * } * } * ``` * - * @class LifetimesService + * @class CachePolicy * @public * @module @ember-data/request-utils */ -export class LifetimesService { - declare config: LifetimesConfig; - declare _stores: WeakMap; types: Map> }>; - - _getStore(store: Store): { invalidated: Set; types: Map> } { +export class CachePolicy { + declare config: PolicyConfig; + declare _stores: WeakMap< + Store, + { invalidated: Set; types: Map> } + >; + + _getStore(store: Store): { + invalidated: Set; + types: Map>; + } { let set = this._stores.get(store); if (!set) { set = { invalidated: new Set(), types: new Map() }; @@ -717,39 +748,33 @@ export class LifetimesService { return set; } - constructor(config: LifetimesConfig) { + constructor(config: PolicyConfig) { this._stores = new WeakMap(); - const _config = arguments.length === 1 ? config : (arguments[1] as unknown as LifetimesConfig); + const _config = arguments.length === 1 ? config : (arguments[1] as unknown as PolicyConfig); deprecate( - `Passing a Store to the LifetimesService is deprecated, please pass only a config instead.`, - arguments.length === 1, + `Passing a Store to the CachePolicy is deprecated, please pass only a config instead.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : arguments.length === 1, { id: 'ember-data:request-utils:lifetimes-service-store-arg', since: { enabled: '5.4', - available: '5.4', + available: '4.13', }, for: '@ember-data/request-utils', until: '6.0', } ); - assert(`You must pass a config to the LifetimesService`, _config); - assert( - `You must pass a apiCacheSoftExpires to the LifetimesService`, - typeof _config.apiCacheSoftExpires === 'number' - ); - assert( - `You must pass a apiCacheHardExpires to the LifetimesService`, - typeof _config.apiCacheHardExpires === 'number' - ); + assert(`You must pass a config to the CachePolicy`, _config); + assert(`You must pass a apiCacheSoftExpires to the CachePolicy`, typeof _config.apiCacheSoftExpires === 'number'); + assert(`You must pass a apiCacheHardExpires to the CachePolicy`, typeof _config.apiCacheHardExpires === 'number'); this.config = _config; } /** * Invalidate a request by its identifier for a given store instance. * - * While the store argument may seem redundant, the lifetimes service + * While the store argument may seem redundant, the CachePolicy * is designed to be shared across multiple stores / forks * of the store. * @@ -763,14 +788,14 @@ export class LifetimesService { * @param {Store} store */ invalidateRequest(identifier: StableDocumentIdentifier, store: Store): void { - this._getStore(store).invalidated.add(identifier.lid); + this._getStore(store).invalidated.add(identifier); } /** * Invalidate all requests associated to a specific type * for a given store instance. * - * While the store argument may seem redundant, the lifetimes service + * While the store argument may seem redundant, the CachePolicy * is designed to be shared across multiple stores / forks * of the store. * @@ -789,9 +814,13 @@ export class LifetimesService { invalidateRequestsForType(type: string, store: Store): void { const storeCache = this._getStore(store); const set = storeCache.types.get(type); + const notifications = store.notifications; + if (set) { + // TODO batch notifications set.forEach((id) => { storeCache.invalidated.add(id); + notifications.notify(id, 'invalidated'); }); } } @@ -842,10 +871,10 @@ export class LifetimesService { request.cacheOptions?.types.forEach((type) => { const set = storeCache.types.get(type); if (set) { - set.add(identifier.lid); - storeCache.invalidated.delete(identifier.lid); + set.add(identifier); + storeCache.invalidated.delete(identifier); } else { - storeCache.types.set(type, new Set([identifier.lid])); + storeCache.types.set(type, new Set([identifier])); } }); } @@ -871,7 +900,7 @@ export class LifetimesService { isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean { // if we are explicitly invalidated, we are hard expired const storeCache = this._getStore(store); - if (storeCache.invalidated.has(identifier.lid)) { + if (storeCache.invalidated.has(identifier)) { return true; } const cache = store.cache; @@ -901,3 +930,22 @@ export class LifetimesService { return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheSoftExpires); } } + +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.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-lifetimes-service-import', + since: { + enabled: '5.4', + available: '4.13', + }, + for: 'ember-data', + until: '6.0', + } + ); + super(config); + } +} diff --git a/packages/request-utils/src/string.ts b/packages/request-utils/src/string.ts new file mode 100644 index 00000000000..d7b0586ac7b --- /dev/null +++ b/packages/request-utils/src/string.ts @@ -0,0 +1,15 @@ +export { + pluralize, + singularize, + singular, + plural, + loadIrregular, + loadUncountable, + irregular, + uncountable, + resetToDefaults, + clear, + clearRules, +} from './-private/string/inflect'; + +export { dasherize, camelize, capitalize, underscore, setMaxLRUCacheSize } from './-private/string/transform'; diff --git a/packages/request-utils/tsconfig.json b/packages/request-utils/tsconfig.json new file mode 100644 index 00000000000..95432a7eb43 --- /dev/null +++ b/packages/request-utils/tsconfig.json @@ -0,0 +1,55 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "declarationDir": "unstable-preview-types", + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/request-utils/vite.config.mjs b/packages/request-utils/vite.config.mjs new file mode 100644 index 00000000000..d12a6fe3d95 --- /dev/null +++ b/packages/request-utils/vite.config.mjs @@ -0,0 +1,12 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = ['@ember/debug', 'ember-inflector']; +export const entryPoints = ['src/index.ts', 'src/string.ts', 'src/deprecation-support.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/request/CHANGELOG.md b/packages/request/CHANGELOG.md new file mode 100644 index 00000000000..d4ea41db09e --- /dev/null +++ b/packages/request/CHANGELOG.md @@ -0,0 +1,83 @@ +# @ember-data/request Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) +* [#9298](https://github.com/emberjs/data/pull/9298) docs(request): remove duplicate line in readme ([@Yelinz](https://github.com/Yelinz)) +* [#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 + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9443](https://github.com/emberjs/data/pull/9443) feat: universal consts ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9459](https://github.com/emberjs/data/pull/9459) fix: ensure cachehandler responses are cast to documents ([@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)) +* [#9360](https://github.com/emberjs/data/pull/9360) fix: Make IS_MAYBE_MIRAGE work in Firefox ([@MichalBryxi](https://github.com/MichalBryxi)) +* [#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)) +* [#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)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9385](https://github.com/emberjs/data/pull/9385) fix: Make IS_MAYBE_MIRAGE simplified ([@MichalBryxi](https://github.com/MichalBryxi)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (6) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) +Sylvain Mina ([@sly7-7](https://github.com/sly7-7)) +Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) +Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@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)) +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@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)) +* [#8921](https://github.com/emberjs/data/pull/8921) feat: Improved Fetch Errors ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9203](https://github.com/emberjs/data/pull/9203) fix: Fetch handler hacks for Mirage (canary) ([@gitKrystan](https://github.com/gitKrystan)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@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: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/request/README.md b/packages/request/README.md index 52f152c17f6..72d246df68c 100644 --- a/packages/request/README.md +++ b/packages/request/README.md @@ -19,6 +19,21 @@ This package provides [*Ember***Data**](https://github.com/emberjs/data/)'s `RequestManager`, a framework agnostic library that can be integrated with any Javascript application to make [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) happen. +- [Installation](#installation) +- [Basic Usage](#🚀-basic-usage) +- [Architecture](#🪜-architecture) +- [Usage](#usage) + - [Making Requests](#making-requests) + - [Using The Response](#using-the-response) + - [Request Handlers](#handling-requests) + - [Handling Errors](#handling-errors) + - [Handling Abort](#handling-abort) + - [Stream Currying](#stream-currying) + - [Automatic Currying](#automatic-currying-of-stream-and-response) + - [Using as a Service](#using-as-a-service) + - [Using with `@ember-data/store`](#using-with-ember-datastore) + - [Using with `ember-data`](#using-with-ember-data) + ## Installation Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) @@ -27,6 +42,15 @@ Install using your javascript package manager of choice. For instance with [pnpm pnpm add @ember-data/request ``` +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/request/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/request/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/request/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/request/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/request/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + ## 🚀 Basic Usage A `RequestManager` provides a request/response flow in which configured handlers are successively given the opportunity to handle, modify, or pass-along a request. @@ -103,6 +127,8 @@ flowchart LR style E color:#58a6ff; ``` +--- + ## Usage ```ts @@ -113,8 +139,9 @@ const userList = await manager.request({ const users = userList.content; ``` -
- Making Requests +--- + +### Making Requests `RequestManager` has a single asyncronous method as it's API: `request` @@ -148,11 +175,12 @@ interface RequestInfo extends FetchOptions { } ``` -> **note:** providing a `signal` is unnecessary as an `AbortController` is automatically provided if none is present. +> **note** +> providing a `signal` is unnecessary as an `AbortController` is automatically provided if none is present. + +--- -
-
- Using the Response
+#### Using the Response `manager.request` returns a `Future`, which allows access to limited information about the request while it is still pending and fulfills with the final state when the request completes and the response has been read. @@ -209,11 +237,9 @@ interface ResponseInfo { } ``` -
+--- -

Handling Requests

-
- { async request(context, next): T; }
+### Request Handlers Requests are fulfilled by handlers. A handler receives the request context as well as a `next` function with which to pass along a request to the next @@ -272,70 +298,68 @@ 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. -
- -
- Error Handling
- - Each handler in the chain can catch errors from upstream and choose to - either handle the error, re-throw the error, or throw a new error. - - ```ts - const MAX_RETRIES = 5; - - const Handler = { - async request(context, next) { - let attempts = 0; - - while (attempts < MAX_RETRIES) { - attempts++; - try { - const response = await next(context.request); - return response; - } catch (e) { - if (isTimeoutError(e) && attempts < MAX_RETRIES) { - // retry request - continue; - } - // rethrow if it is not a timeout error - throw e; +--- + +#### Handling Errors + +Each handler in the chain can catch errors from upstream and choose to +either handle the error, re-throw the error, or throw a new error. + +```ts +const MAX_RETRIES = 5; + +const Handler = { + async request(context, next) { + let attempts = 0; + + while (attempts < MAX_RETRIES) { + attempts++; + try { + const response = await next(context.request); + return response; + } catch (e) { + if (isTimeoutError(e) && attempts < MAX_RETRIES) { + // retry request + continue; } + // rethrow if it is not a timeout error + throw e; } } } - ``` -
+} +``` -
- Handling Abort
+--- + +#### Handling Abort - Aborting a request will reject the current handler in the chain. However, - every handler can potentially catch this error. If your handler needs to - separate AbortError from other Error types, it is recommended to check - `context.request.signal.aborted` (or if a custom controller was supplied `controller.signal.aborted`). +Aborting a request will reject the current handler in the chain. However, +every handler can potentially catch this error. If your handler needs to +separate AbortError from other Error types, it is recommended to check +`context.request.signal.aborted` (or if a custom controller was supplied `controller.signal.aborted`). - In this manner it is possible for a request to recover from an abort and - still proceed; however, as a best practice this should be used for necessary - cleanup only and the original AbortError rethrown if the abort signal comes - from the root controller. +In this manner it is possible for a request to recover from an abort and +still proceed; however, as a best practice this should be used for necessary +cleanup only and the original AbortError rethrown if the abort signal comes +from the root controller. - **AbortControllers are Always Present and Always Entangled** +**AbortControllers are Always Present and Always Entangled** - If the initial request does not supply an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), one will be generated. +If the initial request does not supply an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), one will be generated. - The [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for this controller is automatically added to the request passed into the first handler. +The [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for this controller is automatically added to the request passed into the first handler. - Each handler has the option to supply a new controller to the request when calling `next`. If a new controller is provided it will be automatically - entangled with the root controller. If the root controller aborts, so will - any entangled controllers. +Each handler has the option to supply a new controller to the request when calling `next`. If a new controller is provided it will be automatically +entangled with the root controller. If the root controller aborts, so will +any entangled controllers. - If an entangled controller aborts, the root controller will not abort. This - allows for advanced request-flow scenarios to abort subsections of the request tree without aborting the entire request. +If an entangled controller aborts, the root controller will not abort. This +allows for advanced request-flow scenarios to abort subsections of the request tree without aborting the entire request. -
+--- -
- Stream Currying
+#### Stream Currying `RequestManager.request` and `next` differ from `fetch` in one **crucial detail** in that the outer Promise resolves only once the response stream has been processed. @@ -371,10 +395,11 @@ Handlers that either call `next` multiple times or otherwise have reason to crea Of course, any handler may choose to read and handle the stream, and return either no stream or a different stream in the process. -
+To conditionally stream, you can check if the user has requested the stream with `context.hasRequestedStream`. + +--- -
- Automatic Currying of Stream and Response
+#### Automatic Currying of Stream and Response In order to simplify the common case for handlers which decorate a request, if `next` is called only a single time and `setResponse` was never called by the handler, the response set by the next handler in the chain will be applied to that handler's outcome. For instance, this makes the following pattern possible `return (await next()).content;`. @@ -384,7 +409,7 @@ Finally, if the return value of a handler is a `Future`, we curry `content` and In the case of the `Future` being returned, `Stream` proxying is automatic and immediate and does not wait for the `Future` to resolve. -
+--- ### Using as a Service @@ -394,7 +419,7 @@ Most applications will desire to have a single `RequestManager` instance, which ```ts import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -import Auth from 'ember-simple-auth/ember-data-handler'; +import Auth from 'app/services/auth-handler'; export default class extends RequestManager { constructor(createArgs) { @@ -404,7 +429,9 @@ export default class extends RequestManager { } ``` -### Using with `@ember-data/store` +--- + +#### Using with `@ember-data/store` To have a request service unique to a Store: @@ -424,13 +451,21 @@ class extends Store { } ``` -### Using with `ember-data` +--- + +#### Using with `ember-data` -If using the package [ember-data](https://github.com/emberjs/data/tree/main/packages/-ember-data), the following configuration will automatically be done in order to preserve the legacy [Adapter](https://github.com/emberjs/data/tree/main/packages/adapter) and [Serializer](https://github.com/emberjs/data/tree/main/packages/serializer) behavior. Additional handlers or a service injection like the above would need to be done by the consuming application in order to make broader use of `RequestManager`. +If using the package [ember-data](https://github.com/emberjs/data/tree/main/packages/-ember-data), +the following configuration will automatically be done in order to preserve the +legacy [Adapter](https://github.com/emberjs/data/tree/main/packages/adapter) and +[Serializer](https://github.com/emberjs/data/tree/main/packages/serializer) behavior. +Additional handlers or a service injection like the above would need to be done by the +consuming application in order to make broader use of `RequestManager`. ```ts -import Store, { 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 { @@ -438,12 +473,15 @@ export default class extends Store { constructor(args) { super(args); - this.requestManager.use([LegacyNetworkHandler]); + this.requestManager.use([LegacyNetworkHandler, Fetch]); this.requestManager.useCache(CacheHandler); } } ``` -Because the application's store service (if present) will override the store supplied by `ember-data`, all that is required to define your own ordering and handlers is to supply a store service extending from `@ember-data/store` and configure as shown above. +To provide a different configuration, import and extend `ember-data/store`. The +default configuration will be ignored if the `requestManager` property is set, +though the store will still register the CacheHandler. -For usage of the store's `requestManager` via `store.request()` see the [Store](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fstore) documentation. +For usage of the store's `requestManager` via `store.request()` see the +[Store](https://api.emberjs.com/ember-data/release/modules/@ember-data%2Fstore) documentation. diff --git a/packages/request/addon-main.cjs b/packages/request/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/request/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/request/addon-main.js b/packages/request/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/request/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/request/babel.config.js b/packages/request/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/request/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/request/babel.config.mjs b/packages/request/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/request/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/request/eslint.config.mjs b/packages/request/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/request/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/request/package.json b/packages/request/package.json index 3ee0e9af054..da070bd1169 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -11,66 +11,75 @@ }, "homepage": "https://github.com/emberjs/data", "bugs": "https://github.com/emberjs/data/issues", - "engines": { - "node": "16.* || >= 18" - }, "keywords": [ "ember-addon" ], - "volta": { - "extends": "../../package.json" - }, - "dependencies": { - "ember-cli-babel": "^7.26.11", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0", - "@ember/test-waiters": "^3.0.2" + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], - "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } }, - "ember-addon": { - "main": "addon-main.js", - "type": "addon", - "version": 1 + "peerDependencies": { + "@warp-drive/core-types": "workspace:*" + }, + "dependencies": { + "@ember/test-waiters": "^3.1.0", + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@glimmer/component": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, - "peerDependencies": {}, - "devDependencies": { - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "walk-sync": "^3.0.0", - "typescript": "^5.0.3" + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 }, "ember": { "edition": "octane" } -} \ No newline at end of file +} diff --git a/packages/request/rollup.config.mjs b/packages/request/rollup.config.mjs deleted file mode 100644 index c091dbcf7a5..00000000000 --- a/packages/request/rollup.config.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: ['@embroider/macros', '@ember/test-waiters'], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'fetch.js']), - - nodeResolve({ extensions: ['.ts'] }), - babel({ - extensions: ['.ts'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/request/src/-private/context.ts b/packages/request/src/-private/context.ts index c5ed99a3203..0194d62d75b 100644 --- a/packages/request/src/-private/context.ts +++ b/packages/request/src/-private/context.ts @@ -1,16 +1,12 @@ -import { DEBUG } from '@ember-data/env'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { ImmutableHeaders, ImmutableRequestInfo, RequestInfo, ResponseInfo } from '@warp-drive/core-types/request'; +import { SkipCache } from '@warp-drive/core-types/request'; import { deepFreeze } from './debug'; import { createDeferred } from './future'; -import type { - DeferredStream, - GodContext, - ImmutableHeaders, - ImmutableRequestInfo, - RequestInfo, - ResponseInfo, -} from './types'; -import { SkipCache } from './types'; +import type { DeferredStream, GodContext } from './types'; export function upgradeHeaders(headers: Headers | ImmutableHeaders): ImmutableHeaders { (headers as ImmutableHeaders).clone = () => { @@ -163,11 +159,15 @@ export class Context { #owner: ContextOwner; declare request: ImmutableRequestInfo; declare id: number; + private declare _isCacheHandler: boolean; + private declare _finalized: boolean; - constructor(owner: ContextOwner) { + constructor(owner: ContextOwner, isCacheHandler: boolean) { this.id = owner.requestId; this.#owner = owner; this.request = owner.enhancedRequest; + this._isCacheHandler = isCacheHandler; + this._finalized = false; } setStream(stream: ReadableStream | Promise) { this.#owner.setStream(stream); @@ -176,8 +176,20 @@ export class Context { this.#owner.setResponse(response); } + setIdentifier(identifier: StableDocumentIdentifier) { + assert( + `setIdentifier may only be used synchronously from a CacheHandler`, + identifier && this._isCacheHandler && !this._finalized + ); + this.#owner.god.identifier = identifier; + } + get hasRequestedStream() { return this.#owner.hasRequestedStream; } + + _finalize() { + this._finalized = true; + } } export type HandlerRequestContext = Context; diff --git a/packages/request/src/-private/debug.ts b/packages/request/src/-private/debug.ts index b8e706d14b7..22c7f475be7 100644 --- a/packages/request/src/-private/debug.ts +++ b/packages/request/src/-private/debug.ts @@ -1,7 +1,8 @@ -import { DEBUG } from '@ember-data/env'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; +import type { ImmutableHeaders, RequestInfo } from '@warp-drive/core-types/request'; import { Context, upgradeHeaders } from './context'; -import type { ImmutableHeaders, RequestInfo } from './types'; const BODY_TYPES = { type: 'string', @@ -69,8 +70,8 @@ const ValidKeys = new Map([ ], ]); -const IS_FROZEN = Symbol('FROZEN'); -const IS_COLLECTION = Symbol.for('Collection'); +const IS_FROZEN = getOrSetGlobal('IS_FROZEN', Symbol('FROZEN')); +const IS_COLLECTION = getOrSetGlobal('IS_COLLECTION', Symbol.for('Collection')); function freezeHeaders(headers: Headers | ImmutableHeaders): ImmutableHeaders { headers.delete = @@ -106,7 +107,7 @@ export function deepFreeze(value: T): T { return value; } const arr = (value as unknown[]).map(deepFreeze); - (arr as unknown[] & { [IS_FROZEN]: true })[IS_FROZEN] = true; + arr[IS_FROZEN as unknown as number] = true; return Object.freeze(arr) as T; } case 'null': @@ -133,7 +134,6 @@ export function deepFreeze(value: T): T { case 'error': case 'stream': default: - // eslint-disable-next-line no-console // console.log(`Cannot deep-freeze ${_niceType}`); return value; } @@ -199,7 +199,7 @@ function validateKey(key: string, value: unknown, errors: string[]) { if (typeof value === 'string' || value instanceof ReadableStream) { return; } - let type = niceTypeOf(value); + const type = niceTypeOf(value); if (schema.klass.includes(type)) { return; } @@ -248,7 +248,7 @@ function validateKey(key: string, value: unknown, errors: string[]) { } const keys = Object.keys(value); keys.forEach((k) => { - let v: unknown = (value as Record)[k]; + const v: unknown = (value as Record)[k]; if (typeof k !== 'string') { errors.push(`\tThe key ${String(k)} on ${key} should be a string key`); } else if (typeof v !== 'string') { @@ -334,7 +334,7 @@ export function assertValidRequest( // handle schema const keys = Object.keys(request) as Array; const validationErrors: string[] = []; - const isLegacyRequest: boolean = Boolean('op' in request && !request.url); + const isLegacyRequest = Boolean('op' in request && !request.url); keys.forEach((key) => { if (isLegacyRequest && key === 'data') { return; diff --git a/packages/request/src/-private/future.ts b/packages/request/src/-private/future.ts index 3121af9dffd..53ea1c2cac9 100644 --- a/packages/request/src/-private/future.ts +++ b/packages/request/src/-private/future.ts @@ -1,9 +1,9 @@ +import { IS_FUTURE, type StructuredDocument } from '@warp-drive/core-types/request'; + import type { ContextOwner } from './context'; -import type { Deferred, DeferredFuture, Future, StructuredDataDocument } from './types'; +import type { Deferred, DeferredFuture, Future } from './types'; import { enhanceReason } from './utils'; -export const IS_FUTURE = Symbol('IS_FUTURE'); - export function isFuture(maybe: unknown): maybe is Future { return Boolean(maybe && maybe instanceof Promise && (maybe as Future)[IS_FUTURE] === true); } @@ -18,7 +18,7 @@ export function createDeferred(): Deferred { return { resolve, reject, promise }; } -export function upgradePromise(promise: Promise>, future: Future): Future { +export function upgradePromise(promise: Promise>, future: Future): Future { (promise as Future)[IS_FUTURE] = true; // eslint-disable-next-line @typescript-eslint/unbound-method (promise as Future).getStream = future.getStream; @@ -26,6 +26,8 @@ export function upgradePromise(promise: Promise>, f (promise as Future).abort = future.abort; // eslint-disable-next-line @typescript-eslint/unbound-method (promise as Future).onFinalize = future.onFinalize; + (promise as Future).id = future.id; + (promise as Future).lid = future.lid; return promise as Future; } @@ -51,6 +53,9 @@ export function createFuture(owner: ContextOwner): DeferredFuture { promise.abort = (reason?: string) => { owner.abort(enhanceReason(reason)); }; + promise.id = owner.requestId; + promise.lid = owner.god.identifier; + deferred.promise = promise; return deferred; } diff --git a/packages/request/src/-private/manager.ts b/packages/request/src/-private/manager.ts index b62d7f2a991..df5ed2669b9 100644 --- a/packages/request/src/-private/manager.ts +++ b/packages/request/src/-private/manager.ts @@ -434,15 +434,17 @@ For usage of the store's `requestManager` via `store.request()` see the */ import { importSync } from '@embroider/macros'; -import { DEBUG, TESTING } from '@ember-data/env'; +import { DEBUG, TESTING } from '@warp-drive/build-config/env'; +import { peekUniversalTransient, setUniversalTransient } from '@warp-drive/core-types/-private'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { RequestInfo, StructuredErrorDocument } from '@warp-drive/core-types/request'; import { assertValidRequest } from './debug'; import { upgradePromise } from './future'; import { clearRequestResult, getRequestResult, setPromiseResult } from './promise-cache'; -import type { CacheHandler, Future, GenericCreateArgs, Handler, RequestInfo, StructuredErrorDocument } from './types'; +import type { CacheHandler, Future, GenericCreateArgs, Handler, ManagedRequestPriority } from './types'; import { executeNextHandler, IS_CACHE_HANDLER } from './utils'; -let REQ_ID = 0; /** * ```js * import RequestManager from '@ember-data/request'; @@ -469,8 +471,7 @@ let REQ_ID = 0; * const { apiUrl } = Config; * * // ... create manager - * const manager = new RequestManager(); - * manager.use([Auth, Fetch]); + * const manager = new RequestManager().use([Auth, Fetch]); * * // ... execute a request * const response = await manager.request({ @@ -522,11 +523,21 @@ let REQ_ID = 0; export class RequestManager { #handlers: Handler[] = []; declare _hasCacheHandler: boolean; + /** + * A map of pending requests from request.id to their + * associated CacheHandler promise. + * + * This queue is managed by the CacheHandler + * + * @internal + */ declare _pending: Map>; + declare _deduped: Map }>; constructor(options?: GenericCreateArgs) { Object.assign(this, options); this._pending = new Map(); + this._deduped = new Map(); } /** @@ -539,9 +550,9 @@ export class RequestManager { * @method useCache * @public * @param {Handler[]} cacheHandler - * @return {void} + * @return {ThisType} */ - useCache(cacheHandler: CacheHandler & { [IS_CACHE_HANDLER]?: true }): void { + useCache(cacheHandler: CacheHandler & { [IS_CACHE_HANDLER]?: true }): this { if (DEBUG) { if (this._hasCacheHandler) { throw new Error(`\`RequestManager.useCache()\` May only be invoked once.`); @@ -555,6 +566,7 @@ export class RequestManager { } cacheHandler[IS_CACHE_HANDLER] = true; this.#handlers.unshift(cacheHandler as Handler); + return this; } /** @@ -567,9 +579,9 @@ export class RequestManager { * @method use * @public * @param {Handler[]} newHandlers - * @return {void} + * @return {ThisType} */ - use(newHandlers: Handler[]): void { + use(newHandlers: Handler[]): this { const handlers = this.#handlers; if (DEBUG) { if (Object.isFrozen(handlers)) { @@ -589,6 +601,7 @@ export class RequestManager { }); } handlers.push(...newHandlers); + return this; } /** @@ -601,7 +614,7 @@ export class RequestManager { * @param {RequestInfo} request * @return {Future} */ - request(request: RequestInfo): Future { + request(request: RequestInfo): Future { const handlers = this.#handlers; if (DEBUG) { if (!Object.isFrozen(handlers)) { @@ -615,14 +628,18 @@ export class RequestManager { delete request.controller; } - const requestId = REQ_ID++; - const promise = executeNextHandler(handlers, request, 0, { + const requestId = peekUniversalTransient('REQ_ID') ?? 0; + setUniversalTransient('REQ_ID', requestId + 1); + + const context = { controller, response: null, stream: null, hasRequestedStream: false, id: requestId, - }); + identifier: null, + }; + const promise = executeNextHandler(handlers, request, 0, context); // the cache handler will set the result of the request synchronously // if it is able to fulfill the request from the cache diff --git a/packages/request/src/-private/promise-cache.ts b/packages/request/src/-private/promise-cache.ts index f01896c448a..49202e8c9a5 100644 --- a/packages/request/src/-private/promise-cache.ts +++ b/packages/request/src/-private/promise-cache.ts @@ -1,3 +1,5 @@ +import { getOrSetUniversal } from '@warp-drive/core-types/-private'; + export type CacheResult = { isError: true; result: E } | { isError: false; result: T }; export type Awaitable = { @@ -6,8 +8,8 @@ export type Awaitable = { finally: (onFinally: () => unknown) => unknown; }; -export const PromiseCache = new WeakMap(); -export const RequestMap = new Map(); +export const PromiseCache = getOrSetUniversal('PromiseCache', new WeakMap()); +export const RequestMap = getOrSetUniversal('RequestMap', new Map()); export function setRequestResult(requestId: number, result: CacheResult) { RequestMap.set(requestId, result); diff --git a/packages/request/src/-private/types.ts b/packages/request/src/-private/types.ts index f205334e0cc..27d0243ffdf 100644 --- a/packages/request/src/-private/types.ts +++ b/packages/request/src/-private/types.ts @@ -1,58 +1,24 @@ /* eslint-disable no-irregular-whitespace */ + +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { + IS_FUTURE, + RequestContext, + RequestInfo, + ResponseInfo, + StructuredDataDocument, +} from '@warp-drive/core-types/request'; + /** * @module @ember-data/request */ -import type Store from '@ember-data/store'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; - -interface Request { - controller?: AbortController; - /* Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. */ - cache?: RequestCache; - /* Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. */ - credentials?: RequestCredentials; - /* Returns the kind of resource requested by request, e.g., "document" or "script". */ - destination?: RequestDestination; - /* Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. */ - headers?: Headers; - /* Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] */ - integrity?: string; - /* Returns a boolean indicating whether or not request can outlive the global in which it was created. */ - keepalive?: boolean; - /* Returns request's HTTP method, which is "GET" by default. */ - method?: string; - /* Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. */ - mode?: RequestMode; - /* Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. */ - redirect?: RequestRedirect; - /* Returns the referrer of request. Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the `Referer` header of the request being made. */ - referrer?: string; - /* Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. */ - referrerPolicy?: ReferrerPolicy; - /* Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. */ - signal?: AbortSignal; - /* Returns the URL of request as a string. */ - url?: string; -} -export type ImmutableHeaders = Headers & { clone(): Headers; toJSON(): [string, string][] }; export interface GodContext { controller: AbortController; response: ResponseInfo | null; stream: ReadableStream | Promise | null; hasRequestedStream: boolean; id: number; -} - -export interface StructuredDataDocument { - request: ImmutableRequestInfo; - response: Response | ResponseInfo | null; - content: T; -} -export interface StructuredErrorDocument extends Error { - request: ImmutableRequestInfo; - response: Response | ResponseInfo | null; - error: string | object; - content?: T; + identifier: StableDocumentIdentifier | null; } export type Deferred = { @@ -61,6 +27,8 @@ export type Deferred = { promise: Promise; }; +export type ManagedRequestPriority = { blocking: boolean }; + export type DeferredStream = { resolve(v: ReadableStream | null): void; reject(v: unknown): void; @@ -77,13 +45,14 @@ export type DeferredStream = { * @public */ export type Future = Promise> & { + [IS_FUTURE]: true; /** * Cancel this request by firing the AbortController's signal. * * @method abort * @param {string} [reason] optional reason for aborting the request * @public - * @returns {void} + * @return {void} */ abort(reason?: string): void; /** @@ -91,7 +60,7 @@ export type Future = Promise> & { * * @method getStream * @public - * @returns {Promise} + * @return {Promise} */ getStream(): Promise; @@ -102,9 +71,29 @@ export type Future = Promise> & { * @method onFinalize * @param cb the callback to run * @public - * @returns void + * @return void */ onFinalize(cb: () => void): void; + + /** + * The identifier of the associated request, if any, as + * assigned by the CacheHandler. + * + * @property lid + * @type {StableDocumentIdentifier | null} + * @public + */ + lid: StableDocumentIdentifier | null; + + /** + * The id of the associated request, if any, as assigned + * by the RequestManager + * + * @property id + * @type {number} + * @public + */ + id: number; }; export type DeferredFuture = { @@ -113,98 +102,6 @@ export type DeferredFuture = { promise: Future; }; -export interface RequestInfo extends Request { - cacheOptions?: { key?: string; reload?: boolean; backgroundReload?: boolean }; - store?: Store; - - op?: string; - records?: StableRecordIdentifier[]; - - disableTestWaiter?: boolean; - /* - * data that a handler should convert into - * the query (GET) or body (POST) - */ - data?: Record; - /* - * options specifically intended for handlers - * to utilize to process the request - */ - options?: Record; -} - -export const SkipCache = Symbol.for('ember-data:skip-cache'); - -export interface ImmutableRequestInfo { - readonly cacheOptions?: { - key?: string; - reload?: boolean; - backgroundReload?: boolean; - types?: string[]; - [SkipCache]?: true; - }; - readonly store?: Store; - - readonly op?: string; - readonly records?: StableRecordIdentifier[]; - - readonly disableTestWaiter?: boolean; - /* Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. */ - readonly cache?: RequestCache; - /* Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. */ - readonly credentials?: RequestCredentials; - /* Returns the kind of resource requested by request, e.g., "document" or "script". */ - readonly destination?: RequestDestination; - /* Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. */ - readonly headers?: Headers & { clone(): Headers }; - /* Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] */ - readonly integrity?: string; - /* Returns a boolean indicating whether or not request can outlive the global in which it was created. */ - readonly keepalive?: boolean; - /* Returns request's HTTP method, which is "GET" by default. */ - readonly method?: string; - /* Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. */ - readonly mode?: RequestMode; - /* Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. */ - readonly redirect?: RequestRedirect; - /* Returns the referrer of request. Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default. This is used during fetching to determine the value of the `Referer` header of the request being made. */ - readonly referrer?: string; - /* Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. */ - readonly referrerPolicy?: ReferrerPolicy; - /* Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. */ - readonly signal?: AbortSignal; - /* Returns the URL of request as a string. */ - readonly url?: string; - /* - * data that a handler should convert into - * the query (GET) or body (POST) - */ - readonly data?: Record; - /* - * options specifically intended for handlers - * to utilize to process the request - */ - readonly options?: Record; -} - -export interface ResponseInfo { - readonly headers: ImmutableHeaders; // to do, maybe not this? - readonly ok: boolean; - readonly redirected: boolean; - readonly status: number; - readonly statusText: string; - readonly type: string; - readonly url: string; -} - -export interface RequestContext { - request: ImmutableRequestInfo; - id: number; - - setStream(stream: ReadableStream): void; - setResponse(response: Response | ResponseInfo): void; -} - export type NextFn

= (req: RequestInfo) => Future

; /** diff --git a/packages/request/src/-private/utils.ts b/packages/request/src/-private/utils.ts index 38881528a42..bb771672a13 100644 --- a/packages/request/src/-private/utils.ts +++ b/packages/request/src/-private/utils.ts @@ -1,22 +1,20 @@ -import { DEBUG } from '@ember-data/env'; - -import { Context, ContextOwner } from './context'; -import { assertValidRequest } from './debug'; -import { createFuture, isFuture } from './future'; -import { setRequestResult } from './promise-cache'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; import type { - DeferredFuture, - Future, - GodContext, - Handler, RequestInfo, StructuredDataDocument, + StructuredDocument, StructuredErrorDocument, -} from './types'; +} from '@warp-drive/core-types/request'; +import { STRUCTURED } from '@warp-drive/core-types/request'; -export const STRUCTURED = Symbol('DOC'); -export const IS_CACHE_HANDLER = Symbol('IS_CACHE_HANDLER'); +import { Context, ContextOwner } from './context'; +import { assertValidRequest } from './debug'; +import { createFuture, isFuture } from './future'; +import { setRequestResult } from './promise-cache'; +import type { DeferredFuture, Future, GodContext, Handler } from './types'; +export const IS_CACHE_HANDLER = getOrSetGlobal('IS_CACHE_HANDLER', Symbol('IS_CACHE_HANDLER')); export function curryFuture(owner: ContextOwner, inbound: Future, outbound: DeferredFuture): Future { owner.setStream(inbound.getStream()); @@ -62,6 +60,28 @@ function isDoc(doc: T | StructuredDataDocument): doc is StructuredDataDocu return doc && (doc as StructuredDataDocument)[STRUCTURED] === true; } +function ensureDoc(owner: ContextOwner, content: T | Error, isError: boolean): StructuredDocument { + if (isDoc(content)) { + return content as StructuredDocument; + } + + if (isError) { + return { + [STRUCTURED]: true, + request: owner.request, + response: owner.getResponse(), + error: content as Error, + } as StructuredErrorDocument; + } + + return { + [STRUCTURED]: true, + request: owner.request, + response: owner.getResponse(), + content: content as T, + }; +} + export type HttpErrorProps = { code: number; name: string; @@ -148,18 +168,20 @@ export function executeNextHandler( return executeNextHandler(wares, r, i + 1, god); } - const context = new Context(owner); + const _isCacheHandler = isCacheHandler(wares[i], i); + const context = new Context(owner, _isCacheHandler); let outcome: Promise> | Future; try { outcome = wares[i].request(context, next); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!!outcome && isCacheHandler(wares[i], i)) { + if (_isCacheHandler) { + context._finalize(); + } + if (!!outcome && _isCacheHandler) { if (!(outcome instanceof Promise)) { - setRequestResult(owner.requestId, { isError: false, result: outcome }); + setRequestResult(owner.requestId, { isError: false, result: ensureDoc(owner, outcome, false) }); outcome = Promise.resolve(outcome); } } else if (DEBUG) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!outcome || (!(outcome instanceof Promise) && !(typeof outcome === 'object' && 'then' in outcome))) { // eslint-disable-next-line no-console console.log({ request, handler: wares[i], outcome }); @@ -170,9 +192,10 @@ export function executeNextHandler( } } } catch (e) { - if (isCacheHandler(wares[i], i)) { - setRequestResult(owner.requestId, { isError: true, result: e }); + if (_isCacheHandler) { + setRequestResult(owner.requestId, { isError: true, result: ensureDoc(owner, e, true) }); } + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors outcome = Promise.reject>(e); } const future = createFuture(owner); diff --git a/packages/request/src/fetch.ts b/packages/request/src/fetch.ts index 2cb61b6415c..ed5ef6a6dd4 100644 --- a/packages/request/src/fetch.ts +++ b/packages/request/src/fetch.ts @@ -12,7 +12,7 @@ * @main @ember-data/request/fetch */ -import { DEBUG } from '@ember-data/env'; +import { DEBUG } from '@warp-drive/build-config/env'; import { cloneResponseProperties, type Context } from './-private/context'; import type { HttpErrorProps } from './-private/utils'; @@ -22,10 +22,10 @@ const _fetch: typeof fetch = typeof fetch !== 'undefined' ? (...args) => fetch(...args) : typeof FastBoot !== 'undefined' - ? (...args) => (FastBoot.require('node-fetch') as typeof fetch)(...args) - : ((() => { - throw new Error('No Fetch Implementation Found'); - }) as typeof fetch); + ? (...args) => (FastBoot.require('node-fetch') as typeof fetch)(...args) + : ((() => { + throw new Error('No Fetch Implementation Found'); + }) as typeof fetch); // clones a response in a way that should still // allow it to stream @@ -40,8 +40,7 @@ if (DEBUG) { Boolean( typeof window !== 'undefined' && ((window as { server?: { pretender: unknown } }).server?.pretender || - (window.fetch.toString() !== 'function fetch() { [native code] }' && - window.fetch.toString() !== 'function fetch() {\n [native code]\n}')) + window.fetch.toString().replace(/\s+/g, '') !== 'function fetch() { [native code] }'.replace(/\s+/g, '')) ); } @@ -170,7 +169,6 @@ const Fetch = { context.setStream(stream!.readable); } - // eslint-disable-next-line no-constant-condition while (true) { // we manually read the stream instead of using `response.json()` // or `response.text()` because if we need to stream the body @@ -230,8 +228,8 @@ const Fetch = { const errors = Array.isArray(errorPayload) ? errorPayload : isDict(errorPayload) && Array.isArray(errorPayload.errors) - ? errorPayload.errors - : null; + ? errorPayload.errors + : null; const statusText = response.statusText || ERROR_STATUS_CODE_FOR.get(response.status) || 'Unknown Request Error'; const msg = `[${response.status} ${statusText}] ${context.request.method ?? 'GET'} (${response.type}) - ${ diff --git a/packages/request/src/index.ts b/packages/request/src/index.ts index 20aa9a72700..a88eeaf79fc 100644 --- a/packages/request/src/index.ts +++ b/packages/request/src/index.ts @@ -1,5 +1,14 @@ export { RequestManager as default } from './-private/manager'; export { createDeferred } from './-private/future'; +export type { Future, Handler, CacheHandler, NextFn } from './-private/types'; +export type { + RequestContext, + ImmutableRequestInfo, + RequestInfo, + ResponseInfo, + StructuredDocument, + StructuredErrorDocument, + StructuredDataDocument, +} from '@warp-drive/core-types/request'; export { setPromiseResult, getPromiseResult } from './-private/promise-cache'; export type { Awaitable } from './-private/promise-cache'; -export { SkipCache } from './-private/types'; diff --git a/packages/request/tsconfig.json b/packages/request/tsconfig.json new file mode 100644 index 00000000000..ac4c62d775b --- /dev/null +++ b/packages/request/tsconfig.json @@ -0,0 +1,52 @@ +{ + "include": ["src/**/*", "../../@types/fastboot"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "declarationDir": "unstable-preview-types", + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + "composite": true, + "incremental": true, + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "inlineSources": true, + "baseUrl": ".", + "paths": { + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/request/vite.config.mjs b/packages/request/vite.config.mjs new file mode 100644 index 00000000000..9d67ac8f00d --- /dev/null +++ b/packages/request/vite.config.mjs @@ -0,0 +1,12 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = ['@ember/test-waiters']; +export const entryPoints = ['src/index.ts', 'src/fetch.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md new file mode 100644 index 00000000000..f098f801f04 --- /dev/null +++ b/packages/rest/CHANGELOG.md @@ -0,0 +1,58 @@ +# @ember-data/rest Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@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)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@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)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/rest/LICENSE.md b/packages/rest/LICENSE.md new file mode 100644 index 00000000000..97a483b5d8f --- /dev/null +++ b/packages/rest/LICENSE.md @@ -0,0 +1,11 @@ +The MIT License (MIT) + +Copyright (C) 2017-2023 Ember.js contributors +Portions Copyright (C) 2011-2017 Tilde, Inc. and contributors. +Portions Copyright (C) 2011 LivingSocial Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/rest/README.md b/packages/rest/README.md new file mode 100644 index 00000000000..033df35a80b --- /dev/null +++ b/packages/rest/README.md @@ -0,0 +1,78 @@ +

+ + +

+ +

Elegantly composable. Made for RESTful APIs

+ +This package provides utilities for working with **REST**ful APIs with [*Ember***Data**](https://github.com/emberjs/data/). + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/rest +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/rest/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/rest/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/rest/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/rest/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/rest/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + +## Getting Started + +If this package is how you are first learning about EmberData, we recommend starting with learning about the [Store](https://github.com/emberjs/data/blob/main/packages/store/README.md) and [Requests](https://github.com/emberjs/data/blob/main/packages/request/README.md) + +## Request Builders + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/rest/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/* + => { + url: 'https://api.example.com/v1/emberDevelopers/1?include=friends,pets', + method: 'GET', + headers: , // 'Content-Type': 'application/json;charset=utf-8' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +*/ +``` + +Request builder output may be used with either `requestManager.request` or `store.request`. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common REST format (camelCase pluralized resource types). + +### Available Builders + +- [createRecord]() +- [deleteRecord]() +- [findRecord]() +- [query]() +- [updateRecord]() diff --git a/packages/rest/addon-main.cjs b/packages/rest/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/rest/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/rest/babel.config.mjs b/packages/rest/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/rest/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/rest/ember-data-logo-dark.svg b/packages/rest/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/rest/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/rest/ember-data-logo-light.svg b/packages/rest/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/rest/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/rest/eslint.config.mjs b/packages/rest/eslint.config.mjs new file mode 100644 index 00000000000..3b45156a9d4 --- /dev/null +++ b/packages/rest/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: [], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/rest/package.json b/packages/rest/package.json new file mode 100644 index 00000000000..2815c20498b --- /dev/null +++ b/packages/rest/package.json @@ -0,0 +1,99 @@ +{ + "name": "@ember-data/rest", + "description": "REST Format Support for EmberData", + "version": "4.12.8", + "private": false, + "license": "MIT", + "author": "Chris Thoburn ", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "packages/rest" + }, + "homepage": "https://github.com/emberjs/data", + "bugs": "https://github.com/emberjs/data/issues", + "engines": { + "node": ">= 18.20.4" + }, + "keywords": [ + "ember-addon" + ], + "volta": { + "extends": "../../package.json" + }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "peerDependencies": { + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "^4.12.0 || ^5.0.0", + "@warp-drive/core-types": "workspace:*" + }, + "files": [ + "unstable-preview-types", + "addon-main.cjs", + "dist", + "README.md", + "LICENSE.md", + "ember-data-logo-dark.svg", + "ember-data-logo-light.svg" + ], + "exports": { + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@glimmer/component": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" + }, + "ember": { + "edition": "octane" + }, + "dependenciesMeta": { + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + } + } +} diff --git a/packages/rest/src/-private/builders/-utils.ts b/packages/rest/src/-private/builders/-utils.ts new file mode 100644 index 00000000000..3d711858753 --- /dev/null +++ b/packages/rest/src/-private/builders/-utils.ts @@ -0,0 +1,25 @@ +import type { UrlOptions } from '@ember-data/request-utils'; +import type { CacheOptions, ConstrainedRequestOptions } from '@warp-drive/core-types/request'; + +export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void { + if ('host' in options) { + urlOptions.host = options.host; + } + if ('namespace' in options) { + urlOptions.namespace = options.namespace; + } + if ('resourcePath' in options) { + urlOptions.resourcePath = options.resourcePath; + } +} + +export function extractCacheOptions(options: ConstrainedRequestOptions) { + const cacheOptions: CacheOptions = {}; + if ('reload' in options) { + cacheOptions.reload = options.reload; + } + if ('backgroundReload' in options) { + cacheOptions.backgroundReload = options.backgroundReload; + } + return cacheOptions; +} diff --git a/packages/rest/src/-private/builders/find-record.ts b/packages/rest/src/-private/builders/find-record.ts new file mode 100644 index 00000000000..17e17da5515 --- /dev/null +++ b/packages/rest/src/-private/builders/find-record.ts @@ -0,0 +1,121 @@ +/** + * @module @ember-data/rest/request + */ +import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import { camelize, pluralize } from '@ember-data/request-utils/string'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { + FindRecordOptions, + FindRecordRequestOptions, + RemotelyAccessibleIdentifier, +} from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +/** + * Builds request options to fetch a single resource by a known id or identifier + * configured for the url and header expectations of most REST APIs. + * + * **Basic Usage** + * + * ```ts + * import { findRecord } from '@ember-data/rest/request'; + * + * const data = await store.request(findRecord('person', '1')); + * ``` + * + * **With Options** + * + * ```ts + * import { findRecord } from '@ember-data/rest/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **With an Identifier** + * + * ```ts + * import { findRecord } from '@ember-data/rest/request'; + * + * const options = findRecord({ type: 'person', id: '1' }, { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing and camelCasing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { findRecord } from '@ember-data/rest/request'; + * + * const options = findRecord('person', '1', { include: ['pets', 'friends'] }, { namespace: 'api/v2' }); + * const data = await store.request(options); + * ``` + * + * @method findRecord + * @public + * @static + * @for @ember-data/rest/request + * @param identifier + * @param options + */ +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; + +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord( + identifier: RemotelyAccessibleIdentifier, + options?: FindRecordOptions +): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; +export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; +export function findRecord( + arg1: TypeFromInstance | RemotelyAccessibleIdentifier>, + arg2: string | FindRecordOptions | undefined, + arg3?: FindRecordOptions +): FindRecordRequestOptions> { + const identifier: RemotelyAccessibleIdentifier> = + typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1; + const options: FindRecordOptions = (typeof arg1 === 'string' ? arg3 : (arg2 as FindRecordOptions)) || {}; + const cacheOptions = extractCacheOptions(options); + const urlOptions: FindRecordUrlOptions = { + identifier, + op: 'findRecord', + resourcePath: pluralize(camelize(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url: options.include?.length + ? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}` + : url, + method: 'GET', + headers, + cacheOptions, + op: 'findRecord', + records: [identifier], + }; +} diff --git a/packages/rest/src/-private/builders/query.ts b/packages/rest/src/-private/builders/query.ts new file mode 100644 index 00000000000..8aa749ee3c0 --- /dev/null +++ b/packages/rest/src/-private/builders/query.ts @@ -0,0 +1,102 @@ +/** + * @module @ember-data/rest/request + */ +import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; +import { camelize, pluralize } from '@ember-data/request-utils/string'; +import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { ConstrainedRequestOptions, QueryRequestOptions } from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; + +import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; + +/** + * Builds request options to query for resources, usually by a primary + * type, configured for the url and header expectations of most REST APIs. + * + * **Basic Usage** + * + * ```ts + * import { query } from '@ember-data/rest/request'; + * + * const data = await store.request(query('person')); + * ``` + * + * **With Query Params** + * + * ```ts + * import { query } from '@ember-data/rest/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing and camelCasing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSettings` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { query } from '@ember-data/rest/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }, { reload: true }); + * const data = await store.request(options); + * ``` + * + * @method query + * @public + * @static + * @for @ember-data/rest/request + * @param identifier + * @param query + * @param options + */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query: QueryParamsSource = {}, + options: ConstrainedRequestOptions = {} +): QueryRequestOptions { + const cacheOptions = extractCacheOptions(options); + const urlOptions: QueryUrlOptions = { + identifier: { type }, + op: 'query', + resourcePath: pluralize(camelize(type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + const queryString = buildQueryParams(query, options.urlParamsSettings); + + return { + url: queryString ? `${url}?${queryString}` : url, + method: 'GET', + headers, + cacheOptions, + op: 'query', + }; +} diff --git a/packages/rest/src/-private/builders/save-record.ts b/packages/rest/src/-private/builders/save-record.ts new file mode 100644 index 00000000000..d1e07bdc767 --- /dev/null +++ b/packages/rest/src/-private/builders/save-record.ts @@ -0,0 +1,262 @@ +import { + buildBaseURL, + type CreateRecordUrlOptions, + type DeleteRecordUrlOptions, + type UpdateRecordUrlOptions, +} from '@ember-data/request-utils'; +import { camelize, pluralize } from '@ember-data/request-utils/string'; +import { recordIdentifierFor } from '@ember-data/store'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { + ConstrainedRequestOptions, + CreateRequestOptions, + DeleteRequestOptions, + UpdateRequestOptions, +} from '@warp-drive/core-types/request'; + +import { copyForwardUrlOptions } from './-utils'; + +function isExisting(identifier: StableRecordIdentifier): identifier is StableExistingRecordIdentifier { + return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null; +} + +/** + * Builds request options to delete record for resources, + * configured for the url, method and header expectations of REST APIs. + * + * **Basic Usage** + * + * ```ts + * import { deleteRecord } from '@ember-data/rest/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const data = await store.request(deleteRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { deleteRecord } from '@ember-data/rest/request'; + * + * const person = store.peekRecord('person', '1'); + * + * // mark record as deleted + * store.deleteRecord(person); + * + * // persist deletion + * const options = deleteRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method deleteRecord + * @public + * @static + * @for @ember-data/rest/request + * @param record + * @param options + */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot delete a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: DeleteRecordUrlOptions = { + identifier: identifier, + op: 'deleteRecord', + resourcePath: pluralize(camelize(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: 'DELETE', + headers, + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to create new record for resources, + * configured for the url, method and header expectations of most REST APIs. + * + * **Basic Usage** + * + * ```ts + * import { createRecord } from '@ember-data/rest/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const data = await store.request(createRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { createRecord } from '@ember-data/rest/request'; + * + * const person = store.createRecord('person', { name: 'Ted' }); + * const options = createRecord(person, { namespace: 'api/v1' }); + * const data = await store.request(options); + * ``` + * + * @method createRecord + * @public + * @static + * @for @ember-data/rest/request + * @param record + * @param options + */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + + const urlOptions: CreateRecordUrlOptions = { + identifier: identifier, + op: 'createRecord', + resourcePath: pluralize(camelize(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: 'POST', + headers, + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} + +/** + * Builds request options to update existing record for resources, + * configured for the url, method and header expectations of most REST APIs. + * + * **Basic Usage** + * + * ```ts + * import { updateRecord } from '@ember-data/rest/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const data = await store.request(updateRecord(person)); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `patch` - Allows caller to specify whether to use a PATCH request instead of a PUT request, defaults to `false`. + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's CachePolicy, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { updateRecord } from '@ember-data/rest/request'; + * + * const person = store.peekRecord('person', '1'); + * person.name = 'Chris'; + * const options = updateRecord(person, { patch: true }); + * const data = await store.request(options); + * ``` + * + * @method updateRecord + * @public + * @static + * @for @ember-data/rest/request + * @param record + * @param options + */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options: ConstrainedRequestOptions & { patch?: boolean } = {} +): UpdateRequestOptions { + const identifier = recordIdentifierFor(record); + assert(`Expected to be given a record instance`, identifier); + assert(`Cannot update a record that does not have an associated type and id.`, isExisting(identifier)); + + const urlOptions: UpdateRecordUrlOptions = { + identifier: identifier, + op: 'updateRecord', + resourcePath: pluralize(camelize(identifier.type)), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/json;charset=utf-8'); + + return { + url, + method: options.patch ? 'PATCH' : 'PUT', + headers, + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }; +} diff --git a/tests/adapter-encapsulation/app/components/.gitkeep b/packages/rest/src/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/components/.gitkeep rename to packages/rest/src/.gitkeep diff --git a/packages/rest/src/request.ts b/packages/rest/src/request.ts new file mode 100644 index 00000000000..d1c8c34e436 --- /dev/null +++ b/packages/rest/src/request.ts @@ -0,0 +1,69 @@ +/** + *

+ +

+ +This package provides utilities for working with **REST**ful APIs with [*Ember***Data**](https://github.com/emberjs/data/). + +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +```no-highlight +pnpm add @ember-data/json-api +``` + +## Usage + +Request builders are functions that produce [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +They take a few contextual inputs about the request you want to make, abstracting away the gnarlier details. + +For instance, to fetch a resource from your API + +```ts +import { findRecord } from '@ember-data/rest/request'; + +const options = findRecord('ember-developer', '1', { include: ['pets', 'friends'] }); + +/* + => { + url: 'https://api.example.com/v1/emberDevelopers/1?include=friends,pets', + method: 'GET', + headers: , // 'Content-Type': 'application/json;charset=utf-8' + op: 'findRecord'; + records: [{ type: 'ember-developer', id: '1' }] + } +* / +``` + +Request builder output is ready to go for use with [store.request](https://api.emberjs.com/ember-data/release/classes/Store/methods/request?anchor=request), +[manager.request](https://api.emberjs.com/ember-data/release/classes/RequestManager/methods/request?anchor=request) and most conventional REST APIs. + +Resource types are pluralized and camelized for the url. + +URLs are stable. The same query will produce the same URL every time, even if the order of keys in +the query or values in an array changes. + +URLs follow the most common REST format (camelCase pluralized resource types). + +### Available Builders + +- [createRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Frest/createRecord) +- [deleteRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Frest/deleteRecord) +- [findRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Frest/findRecord) +- [query](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Frest/query) +- [updateRecord](https://api.emberjs.com/ember-data/release/functions/@ember-data%2Frest/updateRecord) + + * @module @ember-data/rest/request + * @main @ember-data/rest/request + * @public + */ +export { findRecord } from './-private/builders/find-record'; +export { query } from './-private/builders/query'; +export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record'; diff --git a/packages/rest/tsconfig.json b/packages/rest/tsconfig.json new file mode 100644 index 00000000000..eb3e30415db --- /dev/null +++ b/packages/rest/tsconfig.json @@ -0,0 +1,66 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../request-utils" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + } + ] +} diff --git a/packages/rest/vite.config.mjs b/packages/rest/vite.config.mjs new file mode 100644 index 00000000000..d1123ac5a32 --- /dev/null +++ b/packages/rest/vite.config.mjs @@ -0,0 +1,12 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = []; +export const entryPoints = ['src/request.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/serializer/.npmignore b/packages/serializer/.npmignore deleted file mode 100644 index e4bce62a5ec..00000000000 --- a/packages/serializer/.npmignore +++ /dev/null @@ -1,40 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# ember-data -/node-tests - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/serializer/CHANGELOG.md b/packages/serializer/CHANGELOG.md new file mode 100644 index 00000000000..ab3085aeb80 --- /dev/null +++ b/packages/serializer/CHANGELOG.md @@ -0,0 +1,68 @@ +# @ember-data/serializer Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) +* [#9401](https://github.com/emberjs/data/pull/9401) feat: preserve lids returned by the API in legacy normalization ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@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)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@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)) + +#### Committers: (3) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Sylvain Mina ([@sly7-7](https://github.com/sly7-7)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9070](https://github.com/emberjs/data/pull/9070) docs: fix note notation to make use of github formatting ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@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)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) +* [#9021](https://github.com/emberjs/data/pull/9021) chore: cleanup ember-data/-private types ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/serializer/README.md b/packages/serializer/README.md index 9f7e4e37e51..3aa85809d9b 100644 --- a/packages/serializer/README.md +++ b/packages/serializer/README.md @@ -31,11 +31,21 @@ If installing `@ember-data/` packages individually install using your javascript pnpm add @ember-data/serializer ``` +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/serializer/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/serializer/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/serializer/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/serializer/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/serializer/lts-4-12?label=%40lts-4-12&color=bbbbbb) + + ## 🚀 Setup If using `ember-data` no additional setup is necesssary. -> **Note** When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below +> **Note** +> When using [ember-data](https://github.com/emberjs/data/blob/main/packages/-ember-data) the below > configuration is handled for you automatically. To use legacy serializers you will need to have installed and configured the LegacyNetworkHandler from [@ember-data/legacy-compat](https://github.com/emberjs/data/blob/main/packages/-ember-data) diff --git a/packages/serializer/addon-main.cjs b/packages/serializer/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/serializer/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/serializer/addon-main.js b/packages/serializer/addon-main.js deleted file mode 100644 index 1dbde47c342..00000000000 --- a/packages/serializer/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/serializer/babel.config.js b/packages/serializer/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/serializer/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/serializer/babel.config.mjs b/packages/serializer/babel.config.mjs new file mode 100644 index 00000000000..c23b859273f --- /dev/null +++ b/packages/serializer/babel.config.mjs @@ -0,0 +1,12 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ['module:decorator-transforms', { runtime: { import: 'decorator-transforms/runtime' } }], + ], +}; diff --git a/packages/serializer/blueprints/serializer-test/index.js b/packages/serializer/blueprints/serializer-test/index.js index ad806f42ed0..a6b662a9a86 100644 --- a/packages/serializer/blueprints/serializer-test/index.js +++ b/packages/serializer/blueprints/serializer-test/index.js @@ -1,15 +1,15 @@ const path = require('path'); const testInfo = require('ember-cli-test-info'); -const useTestFrameworkDetector = require('@ember-data/private-build-infra/src/utilities/test-framework-detector'); -const modulePrefixForProject = require('@ember-data/private-build-infra/src/utilities/module-prefix-for-project'); +const { dasherize } = require('ember-cli-string-utils'); -module.exports = useTestFrameworkDetector({ - description: 'Generates a serializer unit test.', +module.exports = { + description: 'Generates an EmberData Serializer unit test', + supportsAddon() { return false; }, root: __dirname, - fileMapTokens(options) { + fileMapTokens() { return { __root__() { return 'tests'; @@ -21,9 +21,15 @@ module.exports = useTestFrameworkDetector({ }, locals(options) { + const modulePrefix = dasherize(options.project.config().modulePrefix); return { friendlyTestDescription: testInfo.description(options.entity.name, 'Unit', 'Serializer'), - modulePrefix: modulePrefixForProject(options.project), + modulePrefix, }; }, -}); + + filesPath() { + return path.join(__dirname, 'qunit-files') + } +}; + diff --git a/packages/serializer/blueprints/serializer-test/mocha-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/serializer-test/mocha-files/__root__/__path__/__test__.js deleted file mode 100644 index ba73d9cec2f..00000000000 --- a/packages/serializer/blueprints/serializer-test/mocha-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupModelTest } from 'ember-mocha'; - -describe('<%= friendlyTestDescription %>', function () { - setupModelTest('<%= dasherizedModuleName %>', { - // Specify the other units that are required for this test. - needs: ['serializer:<%= dasherizedModuleName %>'], - }); - - // Replace this with your real tests. - it('serializes records', function () { - let record = this.subject(); - - let serializedRecord = record.serialize(); - - expect(serializedRecord).to.be.ok; - }); -}); diff --git a/packages/serializer/blueprints/serializer-test/mocha-rfc-232-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/serializer-test/mocha-rfc-232-files/__root__/__path__/__test__.js deleted file mode 100644 index b8e9b837651..00000000000 --- a/packages/serializer/blueprints/serializer-test/mocha-rfc-232-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from '<%= modulePrefix %>/tests/helpers'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('<%= dasherizedModuleName %>'); - - expect(serializer).to.be.ok; - }); - - it('serializes records', function () { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('<%= dasherizedModuleName %>', {}); - - let serializedRecord = record.serialize(); - - expect(serializedRecord).to.be.ok; - }); -}); diff --git a/packages/serializer/blueprints/serializer-test/qunit-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/serializer-test/qunit-files/__root__/__path__/__test__.js index 4524c134cfe..9997d928613 100644 --- a/packages/serializer/blueprints/serializer-test/qunit-files/__root__/__path__/__test__.js +++ b/packages/serializer/blueprints/serializer-test/qunit-files/__root__/__path__/__test__.js @@ -1,24 +1,23 @@ -import { module, test } from 'qunit'; - import { setupTest } from '<%= modulePrefix %>/tests/helpers'; +import { module, test } from 'qunit'; module('<%= friendlyTestDescription %>', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('<%= dasherizedModuleName %>'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('<%= dasherizedModuleName %>'); - assert.ok(serializer); + assert.ok(serializer, 'serializer exists'); }); test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('<%= dasherizedModuleName %>', {}); + const store = this.owner.lookup('service:store'); + const record = store.createRecord('<%= dasherizedModuleName %>', {}); - let serializedRecord = record.serialize(); + const serializedRecord = record.serialize(); - assert.ok(serializedRecord); + assert.ok(serializedRecord, 'it serializes records'); }); }); diff --git a/packages/serializer/blueprints/serializer/index.js b/packages/serializer/blueprints/serializer/index.js index 1bd401b7eb5..89c418582b3 100644 --- a/packages/serializer/blueprints/serializer/index.js +++ b/packages/serializer/blueprints/serializer/index.js @@ -1,14 +1,80 @@ -const extendFromApplicationEntity = require('@ember-data/private-build-infra/src/utilities/extend-from-application-entity'); -const useEditionDetector = require('@ember-data/private-build-infra/src/utilities/edition-detector'); +const path = require('path'); +const fs = require('fs'); -module.exports = useEditionDetector({ - description: 'Generates an ember-data serializer.', +const stringUtil = require('ember-cli-string-utils'); +const pathUtil = require('ember-cli-path-utils'); + +const { has } = require('@ember/edition-utils'); + +module.exports = { + description: 'Generates an ember-data Serializer.', availableOptions: [{ name: 'base-class', type: String }], root: __dirname, + filesPath() { + let hasOctane = has('octane'); + if (hasOctane && process.env.EMBER_EDITION === 'classic') { + hasOctane = false; //forcible override + } + let rootPath = hasOctane ? 'native-files' : 'files'; + return path.join(__dirname, rootPath); + }, + locals(options) { return extendFromApplicationEntity('serializer', 'JSONAPISerializer', options); }, -}); +}; + +function extendFromApplicationEntity(type, baseClass, options) { + let isAddon = options.inRepoAddon || options.project.isEmberCLIAddon(); + + let entityName = options.entity.name; + let relativePath = pathUtil.getRelativePath(options.entity.name); + + if (options.pod && options.podPath) { + relativePath = pathUtil.getRelativePath(options.podPath + options.entity.name); + } + + let applicationEntityPath = path.join(options.project.root, 'app', `${type}s`, 'application.js'); + + let hasApplicationEntity = fs.existsSync(applicationEntityPath); + if (!isAddon && !options.baseClass && entityName !== 'application' && hasApplicationEntity) { + options.baseClass = 'application'; + } + + if (options.baseClass === entityName) { + throw new Error( + stringUtil.classify(type) + + 's cannot extend from themself. To resolve this, remove the `--base-class` option or change to a different base-class.' + ); + } + + let importStatement; + + if (options.baseClass) { + let baseClassPath = options.baseClass; + baseClass = stringUtil.classify(baseClassPath.replace('/', '-')); + baseClass = baseClass + stringUtil.classify(type); + + importStatement = `import ${baseClass} from '${relativePath}${baseClassPath}';`; + } else { + let baseClassPath = `@ember-data/${type}`; + + if (baseClass.startsWith('JSONAPI')) { + baseClassPath += '/json-api'; + } + + if (baseClass.startsWith('REST')) { + baseClassPath += '/rest'; + } + + importStatement = `import ${baseClass} from '${baseClassPath}';`; + } + + return { + importStatement, + baseClass, + }; +} diff --git a/packages/serializer/blueprints/transform-test/index.js b/packages/serializer/blueprints/transform-test/index.js index 6d90912f4ee..59a47820b03 100644 --- a/packages/serializer/blueprints/transform-test/index.js +++ b/packages/serializer/blueprints/transform-test/index.js @@ -1,15 +1,15 @@ const path = require('path'); const testInfo = require('ember-cli-test-info'); -const useTestFrameworkDetector = require('@ember-data/private-build-infra/src/utilities/test-framework-detector'); -const modulePrefixForProject = require('@ember-data/private-build-infra/src/utilities/module-prefix-for-project'); +const { dasherize } = require('ember-cli-string-utils'); -module.exports = useTestFrameworkDetector({ - description: 'Generates a transform unit test.', +module.exports = { + description: 'Generates an EmberData Transform unit test', + supportsAddon() { return false; }, root: __dirname, - fileMapTokens(options) { + fileMapTokens() { return { __root__() { return 'tests'; @@ -21,9 +21,15 @@ module.exports = useTestFrameworkDetector({ }, locals(options) { + const modulePrefix = dasherize(options.project.config().modulePrefix); return { friendlyTestDescription: testInfo.description(options.entity.name, 'Unit', 'Transform'), - modulePrefix: modulePrefixForProject(options.project), + modulePrefix, }; }, -}); + + filesPath() { + return path.join(__dirname, 'qunit-files') + } +}; + diff --git a/packages/serializer/blueprints/transform-test/mocha-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/transform-test/mocha-files/__root__/__path__/__test__.js deleted file mode 100644 index ff57a63a18c..00000000000 --- a/packages/serializer/blueprints/transform-test/mocha-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,17 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'ember-mocha'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest('transform:<%= dasherizedModuleName %>', { - // Specify the other units that are required for this test. - // needs: ['transform:foo'] - }); - - // Replace this with your real tests. - it('exists', function () { - let transform = this.subject(); - expect(transform).to.be.ok; - }); -}); diff --git a/packages/serializer/blueprints/transform-test/mocha-rfc-232-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/transform-test/mocha-rfc-232-files/__root__/__path__/__test__.js deleted file mode 100644 index 4231b9f5a1a..00000000000 --- a/packages/serializer/blueprints/transform-test/mocha-rfc-232-files/__root__/__path__/__test__.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from '<%= modulePrefix %>/tests/helpers'; - -describe('<%= friendlyTestDescription %>', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let transform = this.owner.lookup('transform:<%= dasherizedModuleName %>'); - expect(transform).to.be.ok; - }); -}); diff --git a/packages/serializer/blueprints/transform-test/qunit-files/__root__/__path__/__test__.js b/packages/serializer/blueprints/transform-test/qunit-files/__root__/__path__/__test__.js index 5010aebee5a..334f2612774 100644 --- a/packages/serializer/blueprints/transform-test/qunit-files/__root__/__path__/__test__.js +++ b/packages/serializer/blueprints/transform-test/qunit-files/__root__/__path__/__test__.js @@ -1,13 +1,12 @@ -import { module, test } from 'qunit'; - import { setupTest } from '<%= modulePrefix %>/tests/helpers'; +import { module, test } from 'qunit'; module('<%= friendlyTestDescription %>', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let transform = this.owner.lookup('transform:<%= dasherizedModuleName %>'); - assert.ok(transform); + const transform = this.owner.lookup('transform:<%= dasherizedModuleName %>'); + assert.ok(transform, 'transform exists'); }); }); diff --git a/packages/serializer/blueprints/transform/index.js b/packages/serializer/blueprints/transform/index.js index f3a140349ec..e0eb47ed08e 100644 --- a/packages/serializer/blueprints/transform/index.js +++ b/packages/serializer/blueprints/transform/index.js @@ -1,7 +1,17 @@ -const useEditionDetector = require('@ember-data/private-build-infra/src/utilities/edition-detector'); +const path = require('path'); -module.exports = useEditionDetector({ - description: 'Generates an ember-data value transform.', +const { has } = require('@ember/edition-utils'); +module.exports = { + description: 'Generates an ember-data Transform.', root: __dirname, -}); + + filesPath() { + let hasOctane = has('octane'); + if (hasOctane && process.env.EMBER_EDITION === 'classic') { + hasOctane = false; //forcible override + } + let rootPath = hasOctane ? 'native-files' : 'files'; + return path.join(__dirname, rootPath); + }, +}; diff --git a/packages/serializer/eslint.config.mjs b/packages/serializer/eslint.config.mjs new file mode 100644 index 00000000000..a8fc9b44513 --- /dev/null +++ b/packages/serializer/eslint.config.mjs @@ -0,0 +1,32 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; + +const AllowedImports = ['@ember/application', '@ember/service', '@ember/debug', '@ember/object', '@ember/object/mixin']; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['src'], + allowedImports: AllowedImports, + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['src'], + allowedImports: AllowedImports, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/serializer/package.json b/packages/serializer/package.json index e56a66e0530..00d97de292d 100644 --- a/packages/serializer/package.json +++ b/packages/serializer/package.json @@ -14,68 +14,106 @@ "author": "", "directories": {}, "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2 }, "files": [ + "unstable-preview-types", "blueprints", - "addon-main.js", - "addon", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./blueprints/*": { + "default": "./blueprints/*.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "peerDependencies": { - "@ember-data/store": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "ember-inflector": "^4.0.2" + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@warp-drive/core-types": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-test-info": "^1.0.0" + "@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", + "@ember/edition-utils": "1.2.0", + "@warp-drive/build-config": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "walk-sync": "^3.0.0", - "typescript": "^5.0.3", - "webpack": "^5.77.0" + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "decorator-transforms": "^2.2.2", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/serializer/rollup.config.mjs b/packages/serializer/rollup.config.mjs deleted file mode 100644 index b5138844c61..00000000000 --- a/packages/serializer/rollup.config.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@ember/service', - '@ember-data/store/-private', - '@ember/object', - '@ember/application', - '@ember/string', - '@ember/debug', - '@ember/polyfills', - '@ember/array', - '@ember/object/mixin', - '@ember/string', - 'ember-inflector', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', 'transform.js', 'json.js', 'json-api.js', 'rest.js', '-private.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/serializer/src/-private.ts b/packages/serializer/src/-private.ts deleted file mode 100644 index 22a34e07500..00000000000 --- a/packages/serializer/src/-private.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -export { default as EmbeddedRecordsMixin } from './-private/embedded-records-mixin'; - -export { default as Transform } from './-private/transforms/transform'; -export { default as BooleanTransform } from './-private/transforms/boolean'; -export { default as DateTransform } from './-private/transforms/date'; -export { default as NumberTransform } from './-private/transforms/number'; -export { default as StringTransform } from './-private/transforms/string'; diff --git a/packages/serializer/src/-private/embedded-records-mixin.js b/packages/serializer/src/-private/embedded-records-mixin.js index ee44876387d..693f286d6f9 100644 --- a/packages/serializer/src/-private/embedded-records-mixin.js +++ b/packages/serializer/src/-private/embedded-records-mixin.js @@ -1,7 +1,7 @@ -import { A } from '@ember/array'; import { warn } from '@ember/debug'; import Mixin from '@ember/object/mixin'; -import { camelize } from '@ember/string'; + +import { camelize } from '@ember-data/request-utils/string'; /** @module @ember-data/serializer/rest @@ -101,7 +101,7 @@ import { camelize } from '@ember/string'; @class EmbeddedRecordsMixin @public */ -export default Mixin.create({ +export const EmbeddedRecordsMixin = Mixin.create({ /** Normalize the record and recursively normalize/extract all the embedded records while pushing them into the store as they are encountered @@ -131,7 +131,7 @@ export default Mixin.create({ @return {Object} the normalized hash **/ normalize(typeClass, hash, prop) { - let normalizedHash = this._super(typeClass, hash, prop); + const normalizedHash = this._super(typeClass, hash, prop); return this._extractEmbeddedRecords(this, this.store, typeClass, normalizedHash); }, @@ -201,19 +201,19 @@ export default Mixin.create({ @param {Object} relationship */ serializeBelongsTo(snapshot, json, relationship) { - let attr = relationship.key; + const attr = relationship.name; if (this.noSerializeOptionSpecified(attr)) { this._super(snapshot, json, relationship); return; } - let includeIds = this.hasSerializeIdsOption(attr); - let includeRecords = this.hasSerializeRecordsOption(attr); - let embeddedSnapshot = snapshot.belongsTo(attr); + const includeIds = this.hasSerializeIdsOption(attr); + const includeRecords = this.hasSerializeRecordsOption(attr); + const embeddedSnapshot = snapshot.belongsTo(attr); if (includeIds) { - let schema = this.store.modelFor(snapshot.modelName); - let serializedKey = this._getMappedKey(relationship.key, schema); - if (serializedKey === relationship.key && this.keyForRelationship) { - serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.name, schema); + if (serializedKey === relationship.name && this.keyForRelationship) { + serializedKey = this.keyForRelationship(relationship.name, relationship.kind, 'serialize'); } if (!embeddedSnapshot) { @@ -231,11 +231,11 @@ export default Mixin.create({ }, _serializeEmbeddedBelongsTo(snapshot, json, relationship) { - let embeddedSnapshot = snapshot.belongsTo(relationship.key); - let schema = this.store.modelFor(snapshot.modelName); - let serializedKey = this._getMappedKey(relationship.key, schema); - if (serializedKey === relationship.key && this.keyForRelationship) { - serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); + const embeddedSnapshot = snapshot.belongsTo(relationship.name); + const schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.name, schema); + if (serializedKey === relationship.name && this.keyForRelationship) { + serializedKey = this.keyForRelationship(relationship.name, relationship.kind, 'serialize'); } if (!embeddedSnapshot) { @@ -389,17 +389,17 @@ export default Mixin.create({ @param {Object} relationship */ serializeHasMany(snapshot, json, relationship) { - let attr = relationship.key; + const attr = relationship.name; if (this.noSerializeOptionSpecified(attr)) { this._super(snapshot, json, relationship); return; } if (this.hasSerializeIdsOption(attr)) { - let schema = this.store.modelFor(snapshot.modelName); - let serializedKey = this._getMappedKey(relationship.key, schema); - if (serializedKey === relationship.key && this.keyForRelationship) { - serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.name, schema); + if (serializedKey === relationship.name && this.keyForRelationship) { + serializedKey = this.keyForRelationship(relationship.name, relationship.kind, 'serialize'); } json[serializedKey] = snapshot.hasMany(attr, { ids: true }); @@ -421,10 +421,10 @@ export default Mixin.create({ TODO: Make the default in Ember-data 3.0?? */ _serializeHasManyAsIdsAndTypes(snapshot, json, relationship) { - let serializedKey = this.keyForAttribute(relationship.key, 'serialize'); - let hasMany = snapshot.hasMany(relationship.key); + const serializedKey = this.keyForAttribute(relationship.name, 'serialize'); + const hasMany = snapshot.hasMany(relationship.name) || []; - json[serializedKey] = A(hasMany).map(function (recordSnapshot) { + json[serializedKey] = hasMany.map(function (recordSnapshot) { // // I'm sure I'm being utterly naive here. Probably id is a configurable property and // type too, and the modelName has to be normalized somehow. @@ -434,15 +434,15 @@ export default Mixin.create({ }, _serializeEmbeddedHasMany(snapshot, json, relationship) { - let schema = this.store.modelFor(snapshot.modelName); - let serializedKey = this._getMappedKey(relationship.key, schema); - if (serializedKey === relationship.key && this.keyForRelationship) { - serializedKey = this.keyForRelationship(relationship.key, relationship.kind, 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let serializedKey = this._getMappedKey(relationship.name, schema); + if (serializedKey === relationship.name && this.keyForRelationship) { + serializedKey = this.keyForRelationship(relationship.name, relationship.kind, 'serialize'); } warn( `The embedded relationship '${serializedKey}' is undefined for '${snapshot.modelName}' with id '${snapshot.id}'. Please include it in your original payload.`, - typeof snapshot.hasMany(relationship.key) !== 'undefined', + typeof snapshot.hasMany(relationship.name) !== 'undefined', { id: 'ds.serializer.embedded-relationship-undefined' } ); @@ -453,13 +453,12 @@ export default Mixin.create({ Returns an array of embedded records serialized to JSON */ _generateSerializedHasMany(snapshot, relationship) { - let hasMany = snapshot.hasMany(relationship.key); - let manyArray = A(hasMany); - let ret = new Array(manyArray.length); + const hasMany = snapshot.hasMany(relationship.name) || []; + const ret = new Array(hasMany.length); - for (let i = 0; i < manyArray.length; i++) { - let embeddedSnapshot = manyArray[i]; - let embeddedJson = embeddedSnapshot.serialize({ includeId: true }); + for (let i = 0; i < hasMany.length; i++) { + const embeddedSnapshot = hasMany[i]; + const embeddedJson = embeddedSnapshot.serialize({ includeId: true }); this.removeEmbeddedForeignKey(snapshot, embeddedSnapshot, relationship, embeddedJson); ret[i] = embeddedJson; } @@ -486,12 +485,12 @@ export default Mixin.create({ */ removeEmbeddedForeignKey(snapshot, embeddedSnapshot, relationship, json) { if (relationship.kind === 'belongsTo') { - let schema = this.store.modelFor(snapshot.modelName); - let parentRecord = schema.inverseFor(relationship.key, this.store); + const schema = this.store.modelFor(snapshot.modelName); + const parentRecord = schema.inverseFor(relationship.name, this.store); if (parentRecord) { - let name = parentRecord.name; - let embeddedSerializer = this.store.serializerFor(embeddedSnapshot.modelName); - let parentKey = embeddedSerializer.keyForRelationship(name, parentRecord.kind, 'deserialize'); + const name = parentRecord.name; + const embeddedSerializer = this.store.serializerFor(embeddedSnapshot.modelName); + const parentKey = embeddedSerializer.keyForRelationship(name, parentRecord.kind, 'deserialize'); if (parentKey) { delete json[parentKey]; } @@ -503,32 +502,32 @@ export default Mixin.create({ // checks config for attrs option to embedded (always) - serialize and deserialize hasEmbeddedAlwaysOption(attr) { - let option = this.attrsOption(attr); + const option = this.attrsOption(attr); return option && option.embedded === 'always'; }, // checks config for attrs option to serialize ids hasSerializeRecordsOption(attr) { - let alwaysEmbed = this.hasEmbeddedAlwaysOption(attr); - let option = this.attrsOption(attr); + const alwaysEmbed = this.hasEmbeddedAlwaysOption(attr); + const option = this.attrsOption(attr); return alwaysEmbed || (option && option.serialize === 'records'); }, // checks config for attrs option to serialize records hasSerializeIdsOption(attr) { - let option = this.attrsOption(attr); + const option = this.attrsOption(attr); return option && (option.serialize === 'ids' || option.serialize === 'id'); }, // checks config for attrs option to serialize records as objects containing id and types hasSerializeIdsAndTypesOption(attr) { - let option = this.attrsOption(attr); + const option = this.attrsOption(attr); return option && (option.serialize === 'ids-and-types' || option.serialize === 'id-and-type'); }, // checks config for attrs option to serialize records noSerializeOptionSpecified(attr) { - let option = this.attrsOption(attr); + const option = this.attrsOption(attr); return !(option && (option.serialize || option.embedded)); }, @@ -536,13 +535,13 @@ export default Mixin.create({ // a defined option object for a resource is treated the same as // `deserialize: 'records'` hasDeserializeRecordsOption(attr) { - let alwaysEmbed = this.hasEmbeddedAlwaysOption(attr); - let option = this.attrsOption(attr); + const alwaysEmbed = this.hasEmbeddedAlwaysOption(attr); + const option = this.attrsOption(attr); return alwaysEmbed || (option && option.deserialize === 'records'); }, attrsOption(attr) { - let attrs = this.attrs; + const attrs = this.attrs; return attrs && (attrs[camelize(attr)] || attrs[attr]); }, @@ -569,17 +568,17 @@ export default Mixin.create({ @private */ _extractEmbeddedHasMany(store, key, hash, relationshipMeta) { - let relationshipHash = hash.data?.relationships?.[key]?.data; + const relationshipHash = hash.data?.relationships?.[key]?.data; if (!relationshipHash) { return; } - let hasMany = new Array(relationshipHash.length); + const hasMany = new Array(relationshipHash.length); for (let i = 0; i < relationshipHash.length; i++) { - let item = relationshipHash[i]; - let { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, item); + const item = relationshipHash[i]; + const { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, item); hash.included = hash.included || []; hash.included.push(data); if (included) { @@ -587,9 +586,12 @@ export default Mixin.create({ } hasMany[i] = { id: data.id, type: data.type }; + if (data.lid) { + hasMany[i].lid = data.lid; + } } - let relationship = { data: hasMany }; + const relationship = { data: hasMany }; hash.data.relationships[key] = relationship; }, @@ -598,20 +600,24 @@ export default Mixin.create({ @private */ _extractEmbeddedBelongsTo(store, key, hash, relationshipMeta) { - let relationshipHash = hash.data?.relationships?.[key]?.data; + const relationshipHash = hash.data?.relationships?.[key]?.data; if (!relationshipHash) { return; } - let { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, relationshipHash); + const { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, relationshipHash); hash.included = hash.included || []; hash.included.push(data); if (included) { hash.included = hash.included.concat(included); } - let belongsTo = { id: data.id, type: data.type }; - let relationship = { data: belongsTo }; + const belongsTo = { id: data.id, type: data.type }; + const relationship = { data: belongsTo }; + + if (data.lid) { + belongsTo.lid = data.lid; + } hash.data.relationships[key] = relationship; }, @@ -625,8 +631,8 @@ export default Mixin.create({ if (relationshipMeta.options.polymorphic) { modelName = relationshipHash.type; } - let modelClass = store.modelFor(modelName); - let serializer = store.serializerFor(modelName); + const modelClass = store.modelFor(modelName); + const serializer = store.serializerFor(modelName); return serializer.normalize(modelClass, relationshipHash, null); }, diff --git a/packages/serializer/src/-private/transforms/boolean.js b/packages/serializer/src/-private/transforms/boolean.js deleted file mode 100644 index 758d6c3a3d3..00000000000 --- a/packages/serializer/src/-private/transforms/boolean.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -/** - The `BooleanTransform` class is used to serialize and deserialize - boolean attributes on Ember Data record objects. This transform is - used when `boolean` is passed as the type parameter to the - [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. - - Usage - - ```app/models/user.js - import Model, { attr } from '@ember-data/model'; - - export default class UserModel extends Model { - @attr('boolean') isAdmin; - @attr('string') name; - @attr('string') email; - } - ``` - - By default, the boolean transform only allows for values of `true` or - `false`. You can opt into allowing `null` values for - boolean attributes via `attr('boolean', { allowNull: true })` - - ```app/models/user.js - import Model, { attr } from '@ember-data/model'; - - export default class UserModel extends Model { - @attr('string') email; - @attr('string') username; - @attr('boolean', { allowNull: true }) wantsWeeklyEmail; - } - ``` - - @class BooleanTransform - @public - */ -export default class BooleanTransform { - deserialize(serialized, options) { - if ((serialized === null || serialized === undefined) && options.allowNull === true) { - return null; - } - - let type = typeof serialized; - if (type === 'boolean') { - return serialized; - } else if (type === 'string') { - return /^(true|t|1)$/i.test(serialized); - } else if (type === 'number') { - return serialized === 1; - } else { - return false; - } - } - - serialize(deserialized, options) { - if ((deserialized === null || deserialized === undefined) && options.allowNull === true) { - return null; - } - - return Boolean(deserialized); - } - - static create() { - return new this(); - } -} diff --git a/packages/serializer/src/-private/transforms/boolean.ts b/packages/serializer/src/-private/transforms/boolean.ts new file mode 100644 index 00000000000..841bb0f20fd --- /dev/null +++ b/packages/serializer/src/-private/transforms/boolean.ts @@ -0,0 +1,72 @@ +/** + @module @ember-data/serializer +*/ + +import type { TransformName } from '@warp-drive/core-types/symbols'; + +/** + The `BooleanTransform` class is used to serialize and deserialize + boolean attributes on Ember Data record objects. This transform is + used when `boolean` is passed as the type parameter to the + [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. + + Usage + + ```app/models/user.js + import Model, { attr } from '@ember-data/model'; + + export default class UserModel extends Model { + @attr('boolean') isAdmin; + @attr('string') name; + @attr('string') email; + } + ``` + + By default, the boolean transform only allows for values of `true` or + `false`. You can opt into allowing `null` values for + boolean attributes via `attr('boolean', { allowNull: true })` + + ```app/models/user.js + import Model, { attr } from '@ember-data/model'; + + export default class UserModel extends Model { + @attr('string') email; + @attr('string') username; + @attr('boolean', { allowNull: true }) wantsWeeklyEmail; + } + ``` + + @class BooleanTransform + @public + */ +export class BooleanTransform { + deserialize(serialized: boolean | null | number | string, options?: { allowNull?: boolean }): boolean | null { + if ((serialized === null || serialized === undefined) && options?.allowNull === true) { + return null; + } + + if (typeof serialized === 'boolean') { + return serialized; + } else if (typeof serialized === 'string') { + return /^(true|t|1)$/i.test(serialized); + } else if (typeof serialized === 'number') { + return serialized === 1; + } else { + return false; + } + } + + serialize(deserialized: boolean | null, options?: { allowNull?: boolean }): boolean | null { + if ((deserialized === null || deserialized === undefined) && options?.allowNull === true) { + return null; + } + + return Boolean(deserialized); + } + + declare [TransformName]: 'boolean'; + + static create() { + return new this(); + } +} diff --git a/packages/serializer/src/-private/transforms/boolean.type-test.ts b/packages/serializer/src/-private/transforms/boolean.type-test.ts new file mode 100644 index 00000000000..45afb7515d4 --- /dev/null +++ b/packages/serializer/src/-private/transforms/boolean.type-test.ts @@ -0,0 +1,11 @@ +import { attr } from '@ember-data/model'; + +import type { BooleanTransform } from './boolean'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class TestModel { + @attr('boolean') declare isAdmin: boolean; + @attr('boolean', {}) declare isOwner: boolean; + @attr('boolean', { allowNull: false }) declare isUser: boolean; + @attr('boolean', { allowNull: true }) declare isPrepared: boolean | null; +} diff --git a/packages/serializer/src/-private/transforms/date.js b/packages/serializer/src/-private/transforms/date.js deleted file mode 100644 index ffe492d474e..00000000000 --- a/packages/serializer/src/-private/transforms/date.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -/** - The `DateTransform` class is used to serialize and deserialize - date attributes on Ember Data record objects. This transform is used - when `date` is passed as the type parameter to the - [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. It uses the [`ISO 8601`](https://en.wikipedia.org/wiki/ISO_8601) - standard. - - ```app/models/score.js - import Model, { attr, belongsTo } from '@ember-data/model'; - - export default class ScoreModel extends Model { - @attr('number') value; - @belongsTo('player') player; - @attr('date') date; - } - ``` - - @class DateTransform - @public - */ - -export default class DateTransform { - deserialize(serialized) { - let type = typeof serialized; - - if (type === 'string') { - let offset = serialized.indexOf('+'); - - if (offset !== -1 && serialized.length - 5 === offset) { - offset += 3; - return new Date(serialized.slice(0, offset) + ':' + serialized.slice(offset)); - } - return new Date(serialized); - } else if (type === 'number') { - return new Date(serialized); - } else if (serialized === null || serialized === undefined) { - // if the value is null return null - // if the value is not present in the data return undefined - return serialized; - } else { - return null; - } - } - - serialize(date) { - if (date instanceof Date && !isNaN(date)) { - return date.toISOString(); - } else { - return null; - } - } - - static create() { - return new this(); - } -} diff --git a/packages/serializer/src/-private/transforms/date.ts b/packages/serializer/src/-private/transforms/date.ts new file mode 100644 index 00000000000..6b04c461797 --- /dev/null +++ b/packages/serializer/src/-private/transforms/date.ts @@ -0,0 +1,63 @@ +/** + @module @ember-data/serializer +*/ + +import { TransformName } from '@warp-drive/core-types/symbols'; + +/** + The `DateTransform` class is used to serialize and deserialize + date attributes on Ember Data record objects. This transform is used + when `date` is passed as the type parameter to the + [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. It uses the [`ISO 8601`](https://en.wikipedia.org/wiki/ISO_8601) + standard. + + ```app/models/score.js + import Model, { attr, belongsTo } from '@ember-data/model'; + + export default class ScoreModel extends Model { + @attr('number') value; + @belongsTo('player') player; + @attr('date') date; + } + ``` + + @class DateTransform + @public + */ + +export class DateTransform { + deserialize(serialized: string | number | null, _options?: Record) { + if (typeof serialized === 'string') { + let offset = serialized.indexOf('+'); + + if (offset !== -1 && serialized.length - 5 === offset) { + offset += 3; + return new Date(serialized.slice(0, offset) + ':' + serialized.slice(offset)); + } + return new Date(serialized); + } else if (typeof serialized === 'number') { + return new Date(serialized); + } else if (serialized === null || serialized === undefined) { + // if the value is null return null + // if the value is not present in the data return undefined + return serialized; + } else { + return null; + } + } + + serialize(date: Date, _options?: Record): string | null { + // @ts-expect-error isNaN accepts date as it is coercible + if (date instanceof Date && !isNaN(date)) { + return date.toISOString(); + } else { + return null; + } + } + + [TransformName] = 'date' as const; + + static create() { + return new this(); + } +} diff --git a/packages/serializer/src/-private/transforms/number.js b/packages/serializer/src/-private/transforms/number.js deleted file mode 100644 index baf86e8fd8b..00000000000 --- a/packages/serializer/src/-private/transforms/number.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -function isNumber(value) { - return value === value && value !== Infinity && value !== -Infinity; -} - -/** - The `NumberTransform` class is used to serialize and deserialize - numeric attributes on Ember Data record objects. This transform is - used when `number` is passed as the type parameter to the - [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. - - Usage - - ```app/models/score.js - import Model, { attr, belongsTo } from '@ember-data/model'; - - export default class ScoreModel extends Model { - @attr('number') value; - @belongsTo('player') player; - @attr('date') date; - } - ``` - - @class NumberTransform - @public - */ -export default class NumberTransform { - deserialize(serialized) { - let transformed; - - if (serialized === '' || serialized === null || serialized === undefined) { - return null; - } else { - transformed = Number(serialized); - - return isNumber(transformed) ? transformed : null; - } - } - - serialize(deserialized) { - let transformed; - - if (deserialized === '' || deserialized === null || deserialized === undefined) { - return null; - } else { - transformed = Number(deserialized); - - return isNumber(transformed) ? transformed : null; - } - } - - static create() { - return new this(); - } -} diff --git a/packages/serializer/src/-private/transforms/number.ts b/packages/serializer/src/-private/transforms/number.ts new file mode 100644 index 00000000000..9cce0d9524d --- /dev/null +++ b/packages/serializer/src/-private/transforms/number.ts @@ -0,0 +1,58 @@ +/** + @module @ember-data/serializer +*/ + +import { TransformName } from '@warp-drive/core-types/symbols'; + +function isNumber(value: number) { + return value === value && value !== Infinity && value !== -Infinity; +} + +/** + The `NumberTransform` class is used to serialize and deserialize + numeric attributes on Ember Data record objects. This transform is + used when `number` is passed as the type parameter to the + [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. + + Usage + + ```app/models/score.js + import Model, { attr, belongsTo } from '@ember-data/model'; + + export default class ScoreModel extends Model { + @attr('number') value; + @belongsTo('player') player; + @attr('date') date; + } + ``` + + @class NumberTransform + @public + */ +export class NumberTransform { + deserialize(serialized: string | number | null | undefined, _options?: Record): number | null { + if (serialized === '' || serialized === null || serialized === undefined) { + return null; + } else { + const transformed = Number(serialized); + + return isNumber(transformed) ? transformed : null; + } + } + + serialize(deserialized: string | number | null | undefined, _options?: Record): number | null { + if (deserialized === '' || deserialized === null || deserialized === undefined) { + return null; + } else { + const transformed = Number(deserialized); + + return isNumber(transformed) ? transformed : null; + } + } + + [TransformName] = 'number' as const; + + static create() { + return new this(); + } +} diff --git a/packages/serializer/src/-private/transforms/string.js b/packages/serializer/src/-private/transforms/string.js deleted file mode 100644 index 7bbd5febfa0..00000000000 --- a/packages/serializer/src/-private/transforms/string.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -/** - The `StringTransform` class is used to serialize and deserialize - string attributes on Ember Data record objects. This transform is - used when `string` is passed as the type parameter to the - [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. - - Usage - - ```app/models/user.js - import Model, { attr, belongsTo } from '@ember-data/model'; - - export default class UserModel extends Model { - @attr('boolean') isAdmin; - @attr('string') name; - @attr('string') email; - } - ``` - - @class StringTransform - @public - */ -export default class StringTransform { - deserialize(serialized) { - return !serialized && serialized !== '' ? null : String(serialized); - } - serialize(deserialized) { - return !deserialized && deserialized !== '' ? null : String(deserialized); - } - - static create() { - return new this(); - } -} diff --git a/packages/serializer/src/-private/transforms/string.ts b/packages/serializer/src/-private/transforms/string.ts new file mode 100644 index 00000000000..57af3e10b10 --- /dev/null +++ b/packages/serializer/src/-private/transforms/string.ts @@ -0,0 +1,41 @@ +/** + @module @ember-data/serializer +*/ + +import { TransformName } from '@warp-drive/core-types/symbols'; + +/** + The `StringTransform` class is used to serialize and deserialize + string attributes on Ember Data record objects. This transform is + used when `string` is passed as the type parameter to the + [attr](/ember-data/release/functions/@ember-data%2Fmodel/attr) function. + + Usage + + ```app/models/user.js + import Model, { attr, belongsTo } from '@ember-data/model'; + + export default class UserModel extends Model { + @attr('boolean') isAdmin; + @attr('string') name; + @attr('string') email; + } + ``` + + @class StringTransform + @public + */ +export class StringTransform { + deserialize(serialized: unknown, _options?: Record): string | null { + return !serialized && serialized !== '' ? null : String(serialized); + } + serialize(deserialized: unknown, _options?: Record): string | null { + return !deserialized && deserialized !== '' ? null : String(deserialized); + } + + [TransformName] = 'string' as const; + + static create() { + return new this(); + } +} diff --git a/packages/serializer/src/-private/transforms/transform.js b/packages/serializer/src/-private/transforms/transform.js deleted file mode 100644 index b421da75dcc..00000000000 --- a/packages/serializer/src/-private/transforms/transform.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - @module @ember-data/serializer -*/ - -/** - The `Transform` class is used to serialize and deserialize model - attributes when they are saved or loaded from an - adapter. Subclassing `Transform` is useful for creating custom - attributes. All subclasses of `Transform` must implement a - `serialize` and a `deserialize` method. - - Example - - ```app/transforms/temperature.js - - // Converts centigrade in the JSON to fahrenheit in the app - export default class TemperatureTransform { - deserialize(serialized, options) { - return (serialized * 1.8) + 32; - } - - serialize(deserialized, options) { - return (deserialized - 32) / 1.8; - } - - static create() { - return new this(); - } - } - ``` - - Usage - - ```app/models/requirement.js - import Model, { attr } from '@ember-data/model'; - - export default class RequirementModel extends Model { - @attr('string') name; - @attr('temperature') temperature; - } - ``` - - The options passed into the `attr` function when the attribute is - declared on the model is also available in the transform. - - ```app/models/post.js - import Model, { attr } from '@ember-data/model'; - - export default class PostModel extends Model { - @attr('string') title; - @attr('markdown', { - markdown: { - gfm: false, - sanitize: true - } - }) - markdown; - } - ``` - - ```app/transforms/markdown.js - export default class MarkdownTransform { - serialize(deserialized, options) { - return deserialized.raw; - } - - deserialize(serialized, options) { - let markdownOptions = options.markdown || {}; - - return marked(serialized, markdownOptions); - } - - static create() { - return new this(); - } - } - ``` - - @class Transform - @public - */ -/** - When given a deserialized value from a record attribute this - method must return the serialized value. - - Example - - ```javascript - serialize(deserialized, options) { - return deserialized ? null : Number(deserialized); - } - ``` - - @method serialize - @public - @param deserialized The deserialized value - @param options hash of options passed to `attr` - @return The serialized value -*/ -/** - When given a serialized value from a JSON object this method must - return the deserialized value for the record attribute. - - Example - - ```javascript - deserialize(serialized, options) { - return empty(serialized) ? null : Number(serialized); - } - ``` - - @method deserialize - @public - @param serialized The serialized value - @param options hash of options passed to `attr` - @return The deserialized value -*/ -export { default } from '@ember/object'; diff --git a/packages/serializer/src/-private/transforms/transform.ts b/packages/serializer/src/-private/transforms/transform.ts new file mode 100644 index 00000000000..170bded3e1a --- /dev/null +++ b/packages/serializer/src/-private/transforms/transform.ts @@ -0,0 +1,125 @@ +/** + @module @ember-data/serializer +*/ +import EmberObject from '@ember/object'; + +import type { LegacyAttributeField } from '@warp-drive/core-types/schema/fields'; + +/** + The `Transform` class is used to serialize and deserialize model + attributes when they are saved or loaded from an + adapter. Subclassing `Transform` is useful for creating custom + attributes. All subclasses of `Transform` must implement a + `serialize` and a `deserialize` method. + + Example + + ```app/transforms/temperature.js + + // Converts centigrade in the JSON to fahrenheit in the app + export default class TemperatureTransform { + deserialize(serialized, options) { + return (serialized * 1.8) + 32; + } + + serialize(deserialized, options) { + return (deserialized - 32) / 1.8; + } + + static create() { + return new this(); + } + } + ``` + + Usage + + ```app/models/requirement.js + import Model, { attr } from '@ember-data/model'; + + export default class RequirementModel extends Model { + @attr('string') name; + @attr('temperature') temperature; + } + ``` + + The options passed into the `attr` function when the attribute is + declared on the model is also available in the transform. + + ```app/models/post.js + import Model, { attr } from '@ember-data/model'; + + export default class PostModel extends Model { + @attr('string') title; + @attr('markdown', { + markdown: { + gfm: false, + sanitize: true + } + }) + markdown; + } + ``` + + ```app/transforms/markdown.js + export default class MarkdownTransform { + serialize(deserialized, options) { + return deserialized.raw; + } + + deserialize(serialized, options) { + let markdownOptions = options.markdown || {}; + + return marked(serialized, markdownOptions); + } + + static create() { + return new this(); + } + } + ``` + + @class Transform + @public + */ +/** + When given a deserialized value from a record attribute this + method must return the serialized value. + + Example + + ```javascript + serialize(deserialized, options) { + return deserialized ? null : Number(deserialized); + } + ``` + + @method serialize + @public + @param deserialized The deserialized value + @param options hash of options passed to `attr` + @return The serialized value +*/ +/** + When given a serialized value from a JSON object this method must + return the deserialized value for the record attribute. + + Example + + ```javascript + deserialize(serialized, options) { + return empty(serialized) ? null : Number(serialized); + } + ``` + + @method deserialize + @public + @param serialized The serialized value + @param options hash of options passed to `attr` + @return The deserialized value +*/ +export interface Transform { + serialize(value: unknown, options: LegacyAttributeField['options']): unknown; + deserialize(value: unknown, options: LegacyAttributeField['options']): unknown; +} +export const Transform = EmberObject; diff --git a/packages/serializer/src/-private/utils.ts b/packages/serializer/src/-private/utils.ts new file mode 100644 index 00000000000..088ae71928b --- /dev/null +++ b/packages/serializer/src/-private/utils.ts @@ -0,0 +1,13 @@ +type Coercable = string | number | boolean | null | undefined | symbol; + +export function coerceId(id: Coercable): string | null { + if (id === null || id === undefined || id === '') { + return null; + } else if (typeof id === 'string') { + return id; + } else if (typeof id === 'symbol') { + return id.toString(); + } else { + return String(id); + } +} diff --git a/packages/serializer/src/index.ts b/packages/serializer/src/index.ts index e43812a97d2..ac177bb11eb 100644 --- a/packages/serializer/src/index.ts +++ b/packages/serializer/src/index.ts @@ -111,6 +111,8 @@ import EmberObject from '@ember/object'; import { inject as service } from '@ember/service'; import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import type { EmptyResourceDocument, SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; /** > ⚠️ CAUTION you likely want the docs for [ Serializer](/ember-data/release/classes/%3CInterface%3E%20Serializer) @@ -264,7 +266,7 @@ export default class extends EmberObject { @param {Object} hash @return {Object} */ - normalize(typeClass, hash) { - return hash; + normalize(_typeClass: ModelSchema, hash: Record): SingleResourceDocument | EmptyResourceDocument { + return hash as unknown as SingleResourceDocument; } } diff --git a/packages/serializer/src/json-api.js b/packages/serializer/src/json-api.js index f04e509cb59..9ea1a4f065b 100644 --- a/packages/serializer/src/json-api.js +++ b/packages/serializer/src/json-api.js @@ -1,12 +1,11 @@ /** * @module @ember-data/serializer/json-api */ -import { assert, warn } from '@ember/debug'; -import { dasherize } from '@ember/string'; +import { warn } from '@ember/debug'; -import { pluralize, singularize } from 'ember-inflector'; - -import { DEBUG } from '@ember-data/env'; +import { dasherize, pluralize, singularize } from '@ember-data/request-utils/string'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; import JSONSerializer from './json'; @@ -144,10 +143,10 @@ const JSONAPISerializer = JSONSerializer.extend({ */ _normalizeDocumentHelper(documentHash) { if (Array.isArray(documentHash.data)) { - let ret = new Array(documentHash.data.length); + const ret = new Array(documentHash.data.length); for (let i = 0; i < documentHash.data.length; i++) { - let data = documentHash.data[i]; + const data = documentHash.data[i]; ret[i] = this._normalizeResourceHelper(data); } @@ -157,10 +156,10 @@ const JSONAPISerializer = JSONSerializer.extend({ } if (Array.isArray(documentHash.included)) { - let ret = new Array(); + const ret = new Array(); for (let i = 0; i < documentHash.included.length; i++) { - let included = documentHash.included[i]; - let normalized = this._normalizeResourceHelper(included); + const included = documentHash.included[i]; + const normalized = this._normalizeResourceHelper(included); if (normalized !== null) { // can be null when unknown type is encountered ret.push(normalized); @@ -194,21 +193,18 @@ const JSONAPISerializer = JSONSerializer.extend({ _normalizeResourceHelper(resourceHash) { assert(this.warnMessageForUndefinedType(), resourceHash.type); - let modelName, usedLookup; - - modelName = this.modelNameFromPayloadKey(resourceHash.type); - usedLookup = 'modelNameFromPayloadKey'; + const type = this.modelNameFromPayloadKey(resourceHash.type); - if (!this.store.getSchemaDefinitionService().doesTypeExist(modelName)) { - warn(this.warnMessageNoModelForType(modelName, resourceHash.type, usedLookup), false, { + if (!this.store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForType(type, resourceHash.type, 'modelNameFromPayloadKey'), false, { id: 'ds.serializer.model-for-type-missing', }); return null; } - let modelClass = this.store.modelFor(modelName); - let serializer = this.store.serializerFor(modelName); - let { data } = serializer.normalize(modelClass, resourceHash); + const modelClass = this.store.modelFor(type); + const serializer = this.store.serializerFor(type); + const { data } = serializer.normalize(modelClass, resourceHash); return data; }, @@ -221,7 +217,7 @@ const JSONAPISerializer = JSONSerializer.extend({ @param {Object} payload */ pushPayload(store, payload) { - let normalizedPayload = this._normalizeDocumentHelper(payload); + const normalizedPayload = this._normalizeDocumentHelper(payload); store.push(normalizedPayload); }, @@ -237,12 +233,12 @@ const JSONAPISerializer = JSONSerializer.extend({ @private */ _normalizeResponse(store, primaryModelClass, payload, id, requestType, isSingle) { - let normalizedPayload = this._normalizeDocumentHelper(payload); + const normalizedPayload = this._normalizeDocumentHelper(payload); return normalizedPayload; }, normalizeQueryRecordResponse() { - let normalized = this._super(...arguments); + const normalized = this._super(...arguments); assert( 'Expected the primary data returned by the serializer for a `queryRecord` response to be a single object but instead it was an array.', @@ -253,11 +249,11 @@ const JSONAPISerializer = JSONSerializer.extend({ }, extractAttributes(modelClass, resourceHash) { - let attributes = {}; + const attributes = {}; if (resourceHash.attributes) { modelClass.eachAttribute((key) => { - let attributeKey = this.keyForAttribute(key, 'deserialize'); + const attributeKey = this.keyForAttribute(key, 'deserialize'); if (resourceHash.attributes[attributeKey] !== undefined) { attributes[key] = resourceHash.attributes[attributeKey]; } @@ -287,10 +283,10 @@ const JSONAPISerializer = JSONSerializer.extend({ */ extractRelationship(relationshipHash) { if (Array.isArray(relationshipHash.data)) { - let ret = new Array(relationshipHash.data.length); + const ret = new Array(relationshipHash.data.length); for (let i = 0; i < relationshipHash.data.length; i++) { - let data = relationshipHash.data[i]; + const data = relationshipHash.data[i]; ret[i] = this._normalizeRelationshipDataHelper(data); } @@ -314,13 +310,13 @@ const JSONAPISerializer = JSONSerializer.extend({ @return {Object} */ extractRelationships(modelClass, resourceHash) { - let relationships = {}; + const relationships = {}; if (resourceHash.relationships) { modelClass.eachRelationship((key, relationshipMeta) => { - let relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); + const relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); if (resourceHash.relationships[relationshipKey] !== undefined) { - let relationshipHash = resourceHash.relationships[relationshipKey]; + const relationshipHash = resourceHash.relationships[relationshipKey]; relationships[key] = this.extractRelationship(relationshipHash); } if (DEBUG) { @@ -364,7 +360,7 @@ const JSONAPISerializer = JSONSerializer.extend({ @return {String} the model's modelName */ modelNameFromPayloadKey(key) { - return singularize(dasherize(key)); + return dasherize(singularize(key)); }, /** @@ -391,13 +387,17 @@ const JSONAPISerializer = JSONSerializer.extend({ this.normalizeUsingDeclaredMapping(modelClass, resourceHash.relationships); } - let data = { + const data = { id: this.extractId(modelClass, resourceHash), type: this._extractType(modelClass, resourceHash), attributes: this.extractAttributes(modelClass, resourceHash), relationships: this.extractRelationships(modelClass, resourceHash), }; + if (resourceHash.lid) { + data.lid = resourceHash.lid; + } + this.applyTransforms(modelClass, data.attributes); return { data }; @@ -637,25 +637,25 @@ const JSONAPISerializer = JSONSerializer.extend({ @return {Object} json */ serialize(snapshot, options) { - let data = this._super(...arguments); + const data = this._super(...arguments); data.type = this.payloadKeyFromModelName(snapshot.modelName); return { data }; }, serializeAttribute(snapshot, json, key, attribute) { - let type = attribute.type; + const type = attribute.type; if (this._canSerialize(key)) { json.attributes = json.attributes || {}; let value = snapshot.attr(key); if (type) { - let transform = this.transformFor(type); + const transform = this.transformFor(type); value = transform.serialize(value, attribute.options); } - let schema = this.store.modelFor(snapshot.modelName); + const schema = this.store.modelFor(snapshot.modelName); let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key) { @@ -667,24 +667,24 @@ const JSONAPISerializer = JSONSerializer.extend({ }, serializeBelongsTo(snapshot, json, relationship) { - let key = relationship.key; + const name = relationship.name; - if (this._canSerialize(key)) { - let belongsTo = snapshot.belongsTo(key); - let belongsToIsNotNew = belongsTo && !belongsTo.isNew; + if (this._canSerialize(name)) { + const belongsTo = snapshot.belongsTo(name); + const belongsToIsNotNew = belongsTo && !belongsTo.isNew; if (belongsTo === null || belongsToIsNotNew) { json.relationships = json.relationships || {}; - let schema = this.store.modelFor(snapshot.modelName); - let payloadKey = this._getMappedKey(key, schema); - if (payloadKey === key) { - payloadKey = this.keyForRelationship(key, 'belongsTo', 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(name, schema); + if (payloadKey === name) { + payloadKey = this.keyForRelationship(name, 'belongsTo', 'serialize'); } let data = null; if (belongsTo) { - let payloadType = this.payloadKeyFromModelName(belongsTo.modelName); + const payloadType = this.payloadKeyFromModelName(belongsTo.modelName); data = { type: payloadType, @@ -698,26 +698,26 @@ const JSONAPISerializer = JSONSerializer.extend({ }, serializeHasMany(snapshot, json, relationship) { - let key = relationship.key; + const name = relationship.name; - if (this.shouldSerializeHasMany(snapshot, key, relationship)) { - let hasMany = snapshot.hasMany(key); + if (this.shouldSerializeHasMany(snapshot, name, relationship)) { + const hasMany = snapshot.hasMany(name); if (hasMany !== undefined) { json.relationships = json.relationships || {}; - let schema = this.store.modelFor(snapshot.modelName); - let payloadKey = this._getMappedKey(key, schema); - if (payloadKey === key && this.keyForRelationship) { - payloadKey = this.keyForRelationship(key, 'hasMany', 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(name, schema); + if (payloadKey === name && this.keyForRelationship) { + payloadKey = this.keyForRelationship(name, 'hasMany', 'serialize'); } // only serialize has many relationships that are not new - let nonNewHasMany = hasMany.filter((item) => !item.isNew); - let data = new Array(nonNewHasMany.length); + const nonNewHasMany = hasMany.filter((item) => !item.isNew); + const data = new Array(nonNewHasMany.length); for (let i = 0; i < nonNewHasMany.length; i++) { - let item = hasMany[i]; - let payloadType = this.payloadKeyFromModelName(item.modelName); + const item = hasMany[i]; + const payloadType = this.payloadKeyFromModelName(item.modelName); data[i] = { type: payloadType, @@ -741,7 +741,7 @@ if (DEBUG) { !this.isEmbeddedRecordsMixin || this.isEmbeddedRecordsMixinCompatible === true ); - let constructor = this.constructor; + const constructor = this.constructor; warn( `You've defined 'extractMeta' in ${constructor.toString()} which is not used for serializers extending JSONAPISerializer. Read more at https://api.emberjs.com/ember-data/release/classes/JSONAPISerializer on how to customize meta when using JSON API.`, this.extractMeta === JSONSerializer.prototype.extractMeta, diff --git a/packages/serializer/src/json.js b/packages/serializer/src/json.js index 04407088653..ba8c7c98597 100644 --- a/packages/serializer/src/json.js +++ b/packages/serializer/src/json.js @@ -2,12 +2,13 @@ * @module @ember-data/serializer/json */ import { getOwner } from '@ember/application'; -import { assert, warn } from '@ember/debug'; -import { dasherize } from '@ember/string'; +import { warn } from '@ember/debug'; -import { coerceId } from '@ember-data/store/-private'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import { assert } from '@warp-drive/build-config/macros'; import Serializer from '.'; +import { coerceId } from './-private/utils'; const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/; const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/; @@ -199,15 +200,15 @@ const JSONSerializer = Serializer.extend({ @return {Object} data The transformed data object */ applyTransforms(typeClass, data) { - let attributes = typeClass.attributes; + const attributes = typeClass.attributes; typeClass.eachTransformedAttribute((key, typeClass) => { if (data[key] === undefined) { return; } - let transform = this.transformFor(typeClass); - let transformMeta = attributes.get(key); + const transform = this.transformFor(typeClass); + const transformMeta = attributes.get(key); data[key] = transform.deserialize(data[key], transformMeta.options); }); @@ -521,12 +522,12 @@ const JSONSerializer = Serializer.extend({ @private */ _normalizeResponse(store, primaryModelClass, payload, id, requestType, isSingle) { - let documentHash = { + const documentHash = { data: null, included: [], }; - let meta = this.extractMeta(store, primaryModelClass, payload); + const meta = this.extractMeta(store, primaryModelClass, payload); if (meta) { assert( 'The `meta` returned from `extractMeta` has to be an object, not "' + typeof meta + '".', @@ -536,16 +537,16 @@ const JSONSerializer = Serializer.extend({ } if (isSingle) { - let { data, included } = this.normalize(primaryModelClass, payload); + const { data, included } = this.normalize(primaryModelClass, payload); documentHash.data = data; if (included) { documentHash.included = included; } } else { - let ret = new Array(payload.length); + const ret = new Array(payload.length); for (let i = 0, l = payload.length; i < l; i++) { - let item = payload[i]; - let { data, included } = this.normalize(primaryModelClass, item); + const item = payload[i]; + const { data, included } = this.normalize(primaryModelClass, item); if (included) { documentHash.included = documentHash.included.concat(included); } @@ -616,6 +617,10 @@ const JSONSerializer = Serializer.extend({ relationships: this.extractRelationships(modelClass, resourceHash), }; + if (resourceHash.lid) { + data.lid = resourceHash.lid; + } + this.applyTransforms(modelClass, data.attributes); } @@ -632,8 +637,8 @@ const JSONSerializer = Serializer.extend({ @return {String} */ extractId(modelClass, resourceHash) { - let primaryKey = this.primaryKey; - let id = resourceHash[primaryKey]; + const primaryKey = this.primaryKey; + const id = resourceHash[primaryKey]; return coerceId(id); }, @@ -650,7 +655,7 @@ const JSONSerializer = Serializer.extend({ */ extractAttributes(modelClass, resourceHash) { let attributeKey; - let attributes = {}; + const attributes = {}; modelClass.eachAttribute((key) => { attributeKey = this.keyForAttribute(key, 'deserialize'); @@ -687,14 +692,14 @@ const JSONSerializer = Serializer.extend({ relationshipHash.id = coerceId(relationshipHash.id); } - let modelClass = this.store.modelFor(relationshipModelName); + const modelClass = this.store.modelFor(relationshipModelName); if (relationshipHash.type && !modelClass.fields.has('type')) { relationshipHash.type = this.modelNameFromPayloadKey(relationshipHash.type); } return relationshipHash; } - return { id: coerceId(relationshipHash), type: relationshipModelName }; + return { id: coerceId(relationshipHash), type: dasherize(singularize(relationshipModelName)) }; }, /** @@ -733,14 +738,14 @@ const JSONSerializer = Serializer.extend({ @return {Object} */ extractRelationships(modelClass, resourceHash) { - let relationships = {}; + const relationships = {}; modelClass.eachRelationship((key, relationshipMeta) => { let relationship = null; - let relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); + const relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); if (resourceHash[relationshipKey] !== undefined) { let data = null; - let relationshipHash = resourceHash[relationshipKey]; + const relationshipHash = resourceHash[relationshipKey]; if (relationshipMeta.kind === 'belongsTo') { if (relationshipMeta.options.polymorphic) { // extracting a polymorphic belongsTo may need more information @@ -760,7 +765,7 @@ const JSONSerializer = Serializer.extend({ data = new Array(relationshipHash.length); if (relationshipMeta.options.polymorphic) { for (let i = 0, l = relationshipHash.length; i < l; i++) { - let item = relationshipHash[i]; + const item = relationshipHash[i]; data[i] = this.extractPolymorphicRelationship(relationshipMeta.type, item, { key, resourceHash, @@ -769,7 +774,7 @@ const JSONSerializer = Serializer.extend({ } } else { for (let i = 0, l = relationshipHash.length; i < l; i++) { - let item = relationshipHash[i]; + const item = relationshipHash[i]; data[i] = this.extractRelationship(relationshipMeta.type, item); } } @@ -778,9 +783,9 @@ const JSONSerializer = Serializer.extend({ relationship = { data }; } - let linkKey = this.keyForLink(key, relationshipMeta.kind); + const linkKey = this.keyForLink(key, relationshipMeta.kind); if (resourceHash.links && resourceHash.links[linkKey] !== undefined) { - let related = resourceHash.links[linkKey]; + const related = resourceHash.links[linkKey]; relationship = relationship || {}; relationship.links = { related }; } @@ -802,7 +807,7 @@ const JSONSerializer = Serializer.extend({ @return {String} the model's modelName */ modelNameFromPayloadKey(key) { - return dasherize(key); + return dasherize(singularize(key)); }, /** @@ -833,12 +838,12 @@ const JSONSerializer = Serializer.extend({ @private */ normalizeUsingDeclaredMapping(modelClass, hash) { - let attrs = this.attrs; + const attrs = this.attrs; let normalizedKey; let payloadKey; if (attrs) { - for (let key in attrs) { + for (const key in attrs) { normalizedKey = payloadKey = this._getMappedKey(key, modelClass); if (hash[payloadKey] === undefined) { @@ -883,7 +888,7 @@ const JSONSerializer = Serializer.extend({ } ); - let attrs = this.attrs; + const attrs = this.attrs; let mappedKey; if (attrs && attrs[key]) { mappedKey = attrs[key]; @@ -910,7 +915,7 @@ const JSONSerializer = Serializer.extend({ @return {boolean} true if the key can be serialized */ _canSerialize(key) { - let attrs = this.attrs; + const attrs = this.attrs; return !attrs || !attrs[key] || attrs[key].serialize !== false; }, @@ -926,7 +931,7 @@ const JSONSerializer = Serializer.extend({ @return {boolean} true if the key must be serialized */ _mustSerialize(key) { - let attrs = this.attrs; + const attrs = this.attrs; return attrs && attrs[key] && attrs[key].serialize === true; }, @@ -941,12 +946,12 @@ const JSONSerializer = Serializer.extend({ @public @param {Snapshot} snapshot @param {String} key - @param {String} relationshipType + @param {RelationshipSchema} relationship @return {boolean} true if the hasMany relationship should be serialized */ shouldSerializeHasMany(snapshot, key, relationship) { const schema = this.store.modelFor(snapshot.modelName); - let relationshipType = schema.determineRelationshipType(relationship, this.store); + const relationshipType = schema.determineRelationshipType(relationship, this.store); if (this._mustSerialize(key)) { return true; } @@ -1108,7 +1113,7 @@ const JSONSerializer = Serializer.extend({ @return {Object} json */ serialize(snapshot, options) { - let json = {}; + const json = {}; if (options && options.includeId) { const id = snapshot.id; @@ -1144,11 +1149,11 @@ const JSONSerializer = Serializer.extend({ ```app/serializers/application.js import RESTSerializer from '@ember-data/serializer/rest'; - import { decamelize } from '/utils/string-utils'; + import { underscoren} from '/utils/string-utils'; export default class ApplicationSerializer extends RESTSerializer { serializeIntoHash(data, type, snapshot, options) { - let root = decamelize(type.modelName); + let root = underscore(type.modelName); data[root] = this.serialize(snapshot, options); } } @@ -1193,16 +1198,16 @@ const JSONSerializer = Serializer.extend({ */ serializeAttribute(snapshot, json, key, attribute) { if (this._canSerialize(key)) { - let type = attribute.type; + const type = attribute.type; let value = snapshot.attr(key); if (type) { - let transform = this.transformFor(type); + const transform = this.transformFor(type); value = transform.serialize(value, attribute.options); } // if provided, use the mapping provided by `attrs` in // the serializer - let schema = this.store.modelFor(snapshot.modelName); + const schema = this.store.modelFor(snapshot.modelName); let payloadKey = this._getMappedKey(key, schema); if (payloadKey === key && this.keyForAttribute) { @@ -1224,7 +1229,7 @@ const JSONSerializer = Serializer.extend({ export default class PostSerializer extends JSONSerializer { serializeBelongsTo(snapshot, json, relationship) { - let key = relationship.key; + let key = relationship.name; let belongsTo = snapshot.belongsTo(key); key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo", "serialize") : key; @@ -1241,17 +1246,17 @@ const JSONSerializer = Serializer.extend({ @param {Object} relationship */ serializeBelongsTo(snapshot, json, relationship) { - let key = relationship.key; + const name = relationship.name; - if (this._canSerialize(key)) { - let belongsToId = snapshot.belongsTo(key, { id: true }); + if (this._canSerialize(name)) { + const belongsToId = snapshot.belongsTo(name, { id: true }); // if provided, use the mapping provided by `attrs` in // the serializer - let schema = this.store.modelFor(snapshot.modelName); - let payloadKey = this._getMappedKey(key, schema); - if (payloadKey === key && this.keyForRelationship) { - payloadKey = this.keyForRelationship(key, 'belongsTo', 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(name, schema); + if (payloadKey === name && this.keyForRelationship) { + payloadKey = this.keyForRelationship(name, 'belongsTo', 'serialize'); } //Need to check whether the id is there for new&async records @@ -1278,7 +1283,7 @@ const JSONSerializer = Serializer.extend({ export default class PostSerializer extends JSONSerializer { serializeHasMany(snapshot, json, relationship) { - let key = relationship.key; + let key = relationship.name; if (key === 'comments') { return; } else { @@ -1295,17 +1300,17 @@ const JSONSerializer = Serializer.extend({ @param {Object} relationship */ serializeHasMany(snapshot, json, relationship) { - let key = relationship.key; + const name = relationship.name; - if (this.shouldSerializeHasMany(snapshot, key, relationship)) { - let hasMany = snapshot.hasMany(key, { ids: true }); + if (this.shouldSerializeHasMany(snapshot, name, relationship)) { + const hasMany = snapshot.hasMany(name, { ids: true }); if (hasMany !== undefined) { // if provided, use the mapping provided by `attrs` in // the serializer - let schema = this.store.modelFor(snapshot.modelName); - let payloadKey = this._getMappedKey(key, schema); - if (payloadKey === key && this.keyForRelationship) { - payloadKey = this.keyForRelationship(key, 'hasMany', 'serialize'); + const schema = this.store.modelFor(snapshot.modelName); + let payloadKey = this._getMappedKey(name, schema); + if (payloadKey === name && this.keyForRelationship) { + payloadKey = this.keyForRelationship(name, 'hasMany', 'serialize'); } json[payloadKey] = hasMany; @@ -1327,7 +1332,7 @@ const JSONSerializer = Serializer.extend({ export default class CommentSerializer extends JSONSerializer { serializePolymorphicType(snapshot, json, relationship) { - let key = relationship.key; + let key = relationship.name; let belongsTo = snapshot.belongsTo(key); key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key; @@ -1378,7 +1383,7 @@ const JSONSerializer = Serializer.extend({ */ extractMeta(store, modelClass, payload) { if (payload && payload['meta'] !== undefined) { - let meta = payload.meta; + const meta = payload.meta; delete payload.meta; return meta; } @@ -1498,7 +1503,7 @@ const JSONSerializer = Serializer.extend({ // for each attr and relationship, make sure that we use // the normalized key typeClass.eachAttribute((name) => { - let key = this.keyForAttribute(name, 'deserialize'); + const key = this.keyForAttribute(name, 'deserialize'); if (key !== name && extracted[key] !== undefined) { extracted[name] = extracted[key]; delete extracted[key]; @@ -1506,7 +1511,7 @@ const JSONSerializer = Serializer.extend({ }); typeClass.eachRelationship((name) => { - let key = this.keyForRelationship(name, 'deserialize'); + const key = this.keyForRelationship(name, 'deserialize'); if (key !== name && extracted[key] !== undefined) { extracted[name] = extracted[key]; delete extracted[key]; @@ -1599,7 +1604,7 @@ const JSONSerializer = Serializer.extend({ @return {Transform} transform */ transformFor(attributeType, skipAssertion) { - let transform = getOwner(this).lookup('transform:' + attributeType); + const transform = getOwner(this).lookup('transform:' + attributeType); assert(`Unable to find the transform for \`attr('${attributeType}')\``, skipAssertion || !!transform); diff --git a/packages/serializer/src/rest.js b/packages/serializer/src/rest.js index c96aef93314..06b042fc73b 100644 --- a/packages/serializer/src/rest.js +++ b/packages/serializer/src/rest.js @@ -1,14 +1,13 @@ /** * @module @ember-data/serializer/rest */ -import { assert, warn } from '@ember/debug'; -import { camelize, dasherize } from '@ember/string'; +import { warn } from '@ember/debug'; -import { singularize } from 'ember-inflector'; - -import { DEBUG } from '@ember-data/env'; -import { coerceId } from '@ember-data/store/-private'; +import { camelize, dasherize, singularize } from '@ember-data/request-utils/string'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import { coerceId } from './-private/utils'; import JSONSerializer from './json'; function makeArray(value) { @@ -95,7 +94,7 @@ const RESTSerializer = JSONSerializer.extend({ @return {String} normalized key */ keyForPolymorphicType(key, typeClass, method) { - let relationshipKey = this.keyForRelationship(key); + const relationshipKey = this.keyForRelationship(key); return `${relationshipKey}Type`; }, @@ -181,16 +180,16 @@ const RESTSerializer = JSONSerializer.extend({ @private */ _normalizeArray(store, modelName, arrayHash, prop) { - let documentHash = { + const documentHash = { data: [], included: [], }; - let modelClass = store.modelFor(modelName); - let serializer = store.serializerFor(modelName); + const modelClass = store.modelFor(modelName); + const serializer = store.serializerFor(modelName); makeArray(arrayHash).forEach((hash) => { - let { data, included } = this._normalizePolymorphicRecord(store, hash, prop, modelClass, serializer); + const { data, included } = this._normalizePolymorphicRecord(store, hash, prop, modelClass, serializer); documentHash.data.push(data); if (included) { documentHash.included = documentHash.included.concat(included); @@ -204,15 +203,15 @@ const RESTSerializer = JSONSerializer.extend({ let serializer = primarySerializer; let modelClass = primaryModelClass; - let primaryHasTypeAttribute = primaryModelClass.fields.has('type'); + const primaryHasTypeAttribute = primaryModelClass.fields.has('type'); if (!primaryHasTypeAttribute && hash.type) { // Support polymorphic records in async relationships - let modelName = this.modelNameFromPayloadKey(hash.type); + const type = this.modelNameFromPayloadKey(hash.type); - if (store.getSchemaDefinitionService().doesTypeExist(modelName)) { - serializer = store.serializerFor(modelName); - modelClass = store.modelFor(modelName); + if (store.schema.hasResource({ type })) { + serializer = store.serializerFor(type); + modelClass = store.modelFor(type); } } @@ -231,12 +230,12 @@ const RESTSerializer = JSONSerializer.extend({ @private */ _normalizeResponse(store, primaryModelClass, payload, id, requestType, isSingle) { - let documentHash = { + const documentHash = { data: null, included: [], }; - let meta = this.extractMeta(store, primaryModelClass, payload); + const meta = this.extractMeta(store, primaryModelClass, payload); if (meta) { assert( 'The `meta` returned from `extractMeta` has to be an object, not "' + typeof meta + '".', @@ -245,7 +244,7 @@ const RESTSerializer = JSONSerializer.extend({ documentHash.meta = meta; } - let keys = Object.keys(payload); + const keys = Object.keys(payload); for (var i = 0, length = keys.length; i < length; i++) { var prop = keys[i]; @@ -277,15 +276,15 @@ const RESTSerializer = JSONSerializer.extend({ modelName = prop.substr(1); } - var typeName = this.modelNameFromPayloadKey(modelName); - if (!store.getSchemaDefinitionService().doesTypeExist(typeName)) { - warn(this.warnMessageNoModelForKey(modelName, typeName), false, { + const type = this.modelNameFromPayloadKey(modelName); + if (!store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForKey(modelName, type), false, { id: 'ds.serializer.model-for-key-missing', }); continue; } - var isPrimary = !forcedSecondary && this.isPrimaryType(store, typeName, primaryModelClass); + var isPrimary = !forcedSecondary && this.isPrimaryType(store, type, primaryModelClass); var value = payload[prop]; if (value === null) { @@ -309,7 +308,7 @@ const RESTSerializer = JSONSerializer.extend({ ``` */ if (isPrimary && !Array.isArray(value)) { - let { data, included } = this._normalizePolymorphicRecord(store, value, prop, primaryModelClass, this); + const { data, included } = this._normalizePolymorphicRecord(store, value, prop, primaryModelClass, this); documentHash.data = data; if (included) { documentHash.included = documentHash.included.concat(included); @@ -317,7 +316,7 @@ const RESTSerializer = JSONSerializer.extend({ continue; } - let { data, included } = this._normalizeArray(store, typeName, value, prop); + const { data, included } = this._normalizeArray(store, type, value, prop); if (included) { documentHash.included = documentHash.included.concat(included); @@ -334,8 +333,8 @@ const RESTSerializer = JSONSerializer.extend({ 2. If it's a newly created record without an ID, the first record in the array */ - let isUpdatedRecord = isPrimary && coerceId(resource.id) === id; - let isFirstCreatedRecord = isPrimary && !id && !documentHash.data; + const isUpdatedRecord = isPrimary && coerceId(resource.id) === id; + const isFirstCreatedRecord = isPrimary && !id && !documentHash.data; if (isFirstCreatedRecord || isUpdatedRecord) { documentHash.data = resource; @@ -394,24 +393,24 @@ const RESTSerializer = JSONSerializer.extend({ @param {Object} payload */ pushPayload(store, payload) { - let documentHash = { + const documentHash = { data: [], included: [], }; - for (var prop in payload) { - var modelName = this.modelNameFromPayloadKey(prop); - if (!store.getSchemaDefinitionService().doesTypeExist(modelName)) { - warn(this.warnMessageNoModelForKey(prop, modelName), false, { + for (const prop in payload) { + const type = this.modelNameFromPayloadKey(prop); + if (!store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForKey(prop, type), false, { id: 'ds.serializer.model-for-key-missing', }); continue; } - var type = store.modelFor(modelName); - var typeSerializer = store.serializerFor(type.modelName); + const ModelSchema = store.modelFor(type); + const typeSerializer = store.serializerFor(ModelSchema.modelName); makeArray(payload[prop]).forEach((hash) => { - let { data, included } = typeSerializer.normalize(type, hash, prop); + const { data, included } = typeSerializer.normalize(ModelSchema, hash, prop); documentHash.data.push(data); if (included) { documentHash.included = documentHash.included.concat(included); @@ -481,7 +480,7 @@ const RESTSerializer = JSONSerializer.extend({ @return {String} the model's modelName */ modelNameFromPayloadKey(key) { - return singularize(dasherize(key)); + return dasherize(singularize(key)); }, // SERIALIZE @@ -653,11 +652,11 @@ const RESTSerializer = JSONSerializer.extend({ ```app/serializers/application.js import RESTSerializer from '@ember-data/serializer/rest'; - import { decamelize } from '/utils/string-utils'; + import { underscore } from '/utils/string-utils'; export default class ApplicationSerializer extends RESTSerializer { serializeIntoHash(data, type, record, options) { - let root = decamelize(type.modelName); + let root = underscore(type.modelName); data[root] = this.serialize(record, options); } } @@ -671,7 +670,7 @@ const RESTSerializer = JSONSerializer.extend({ @param {Object} options */ serializeIntoHash(hash, typeClass, snapshot, options) { - let normalizedRootKey = this.payloadKeyFromModelName(typeClass.modelName); + const normalizedRootKey = this.payloadKeyFromModelName(typeClass.modelName); hash[normalizedRootKey] = this.serialize(snapshot, options); }, @@ -738,9 +737,9 @@ const RESTSerializer = JSONSerializer.extend({ @param {Object} relationship */ serializePolymorphicType(snapshot, json, relationship) { - let key = relationship.key; - let typeKey = this.keyForPolymorphicType(key, relationship.type, 'serialize'); - let belongsTo = snapshot.belongsTo(key); + const name = relationship.name; + const typeKey = this.keyForPolymorphicType(name, relationship.type, 'serialize'); + const belongsTo = snapshot.belongsTo(name); if (!belongsTo) { json[typeKey] = null; @@ -761,7 +760,7 @@ const RESTSerializer = JSONSerializer.extend({ @return {Object} */ extractPolymorphicRelationship(relationshipType, relationshipHash, relationshipOptions) { - let { key, resourceHash, relationshipMeta } = relationshipOptions; + const { key, resourceHash, relationshipMeta } = relationshipOptions; // A polymorphic belongsTo relationship can be present in the payload // either in the form where the `id` and the `type` are given: @@ -780,13 +779,13 @@ const RESTSerializer = JSONSerializer.extend({ // The next code checks if the latter case is present and returns the // corresponding JSON-API representation. The former case is handled within // the base class JSONSerializer. - let isPolymorphic = relationshipMeta.options.polymorphic; - let typeProperty = this.keyForPolymorphicType(key, relationshipType, 'deserialize'); + const isPolymorphic = relationshipMeta.options.polymorphic; + const typeProperty = this.keyForPolymorphicType(key, relationshipType, 'deserialize'); if (isPolymorphic && resourceHash[typeProperty] !== undefined && typeof relationshipHash !== 'object') { - let type = this.modelNameFromPayloadKey(resourceHash[typeProperty]); + const type = this.modelNameFromPayloadKey(resourceHash[typeProperty]); return { - id: relationshipHash, + id: coerceId(relationshipHash), type: type, }; } @@ -813,6 +812,6 @@ if (DEBUG) { }); } -export { EmbeddedRecordsMixin } from './-private'; +export { EmbeddedRecordsMixin } from './-private/embedded-records-mixin'; export default RESTSerializer; diff --git a/packages/serializer/src/transform.js b/packages/serializer/src/transform.js deleted file mode 100644 index 5b25c64d096..00000000000 --- a/packages/serializer/src/transform.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - @module @ember-data/serializer -*/ -import { Transform } from './-private'; - -export default Transform; diff --git a/packages/serializer/src/transform.ts b/packages/serializer/src/transform.ts new file mode 100644 index 00000000000..5585826c316 --- /dev/null +++ b/packages/serializer/src/transform.ts @@ -0,0 +1,8 @@ +/** + @module @ember-data/serializer +*/ +export { Transform as default } from './-private/transforms/transform'; +export { BooleanTransform } from './-private/transforms/boolean'; +export { DateTransform } from './-private/transforms/date'; +export { NumberTransform } from './-private/transforms/number'; +export { StringTransform } from './-private/transforms/string'; diff --git a/packages/serializer/tsconfig.json b/packages/serializer/tsconfig.json new file mode 100644 index 00000000000..4feab1c9747 --- /dev/null +++ b/packages/serializer/tsconfig.json @@ -0,0 +1,77 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/model": ["../model/unstable-preview-types"], + "@ember-data/model/*": ["../model/unstable-preview-types/*"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../legacy-compat/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../model" + }, + { + "path": "../request" + }, + { + "path": "../store" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + }, + { + "path": "../legacy-compat" + }, + { + "path": "../request-utils" + } + ] +} diff --git a/packages/serializer/vite.config.mjs b/packages/serializer/vite.config.mjs new file mode 100644 index 00000000000..d6dd980d24b --- /dev/null +++ b/packages/serializer/vite.config.mjs @@ -0,0 +1,26 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@ember/service', + '@ember/object', + '@ember/application', + '@ember/debug', + '@ember/polyfills', + '@ember/array', + '@ember/object/mixin', +]; +export const entryPoints = [ + './src/index.ts', + './src/transform.ts', + './src/json.js', + './src/json-api.js', + './src/rest.js', +]; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/store/.npmignore b/packages/store/.npmignore deleted file mode 100644 index fa012abdcb4..00000000000 --- a/packages/store/.npmignore +++ /dev/null @@ -1,37 +0,0 @@ -# compiled output -/dist/ -/dist/**/* -/tmp/ -/types/ -**/*.d.ts - -# dependencies -/bower_components/ - -# misc -/.bowerrc -/.editorconfig -/.ember-cli -/.env* -/.eslintignore -/.eslintrc.js -/.gitignore -/.template-lintrc.js -/.travis.yml -/.watchmanconfig -/bower.json -/config/ember-try.js -/CONTRIBUTING.md -/ember-cli-build.js -/testem.js -/tests/ -/yarn.lock -.gitkeep - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try - -# whitelist yuidoc's data.json for api docs generation -!/dist/docs/data.json \ No newline at end of file diff --git a/packages/store/CHANGELOG.md b/packages/store/CHANGELOG.md new file mode 100644 index 00000000000..bbbccfcda42 --- /dev/null +++ b/packages/store/CHANGELOG.md @@ -0,0 +1,120 @@ +# @ember-data/store Changelog + +## v5.3.4 (2024-06-15) + +#### :evergreen_tree: New Deprecation + +* [#9403](https://github.com/emberjs/data/pull/9403) feat: deprecate store extending EmberObject ([@runspired](https://github.com/runspired)) + +#### :memo: Documentation + +* [#9378](https://github.com/emberjs/data/pull/9378) Update some docs to string ids ([@wagenet](https://github.com/wagenet)) +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9474](https://github.com/emberjs/data/pull/9474) Improve query types for legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@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)) +* [#9396](https://github.com/emberjs/data/pull/9396) fix: Resolve promise types for props passed to `store.createRecord()` ([@seanCodes](https://github.com/seanCodes)) +* [#9387](https://github.com/emberjs/data/pull/9387) feat: better types for legacy store methods ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@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)) +* [#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)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@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)) +* [#9245](https://github.com/emberjs/data/pull/9245) feat: add consumer types for Model APIs ([@runspired](https://github.com/runspired)) +* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9459](https://github.com/emberjs/data/pull/9459) fix: ensure cachehandler responses are cast to documents ([@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)) +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9476](https://github.com/emberjs/data/pull/9476) chore: cleanup symbol usage ([@runspired](https://github.com/runspired)) +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#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)) + +#### Committers: (7) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Peter Wagenet ([@wagenet](https://github.com/wagenet)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) +Sean Juarez ([@seanCodes](https://github.com/seanCodes)) +Rich Glazerman ([@richgt](https://github.com/richgt)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) +Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## 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 + +* [#9162](https://github.com/emberjs/data/pull/9162) feat: improve store.request documentation ([@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)) +* [#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)) + +#### :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)) +* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@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)) +* [#8935](https://github.com/emberjs/data/pull/8935) feat: (private) implement basic field support for schema-record ([@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 + +* [#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)) +* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-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)) +* [#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)) +* [#9016](https://github.com/emberjs/data/pull/9016) chore: make type-only files strict ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@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: (2) + +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) +Chris Thoburn ([@runspired](https://github.com/runspired)) + diff --git a/packages/store/README.md b/packages/store/README.md index 161f9de8a49..64456d52d5c 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -55,12 +55,21 @@ pnpm add @ember-data/store After installing you will want to configure your first `Store`. Read more below for how to create and configure stores for your application. +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/store/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/store/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/store/latest?label=%40latest&color=90EE90) +- ![NPM LTS Version](https://img.shields.io/npm/v/%40ember-data/store/lts?label=%40lts&color=0096FF) +- ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40ember-data/store/lts-4-12?label=%40lts-4-12&color=bbbbbb) + ## 🔨 Creating A Store To use a `Store` we will need to do few things: add a [Cache](https://api.emberjs.com/ember-data/release/classes/%3CInterface%3E%20Cache) to store data **in-memory**, add a [Handler](https://github.com/emberjs/data/tree/main/packages/request#handling-requests) to fetch data from a source, and implement `instantiateRecord` to tell the store how to display the data for individual resources. -> **Note** If you are using the package `ember-data` then a `JSON:API` cache and `instantiateRecord` are configured for you by default. +> **Note** +> If you are using the package `ember-data` then a `JSON:API` cache and `instantiateRecord` are configured for you by default. ### Configuring A Cache @@ -81,7 +90,8 @@ class extends Store { Now that we have a `cache` let's setup something to handle fetching and saving data via our API. -> **Note** The `ember-data` package automatically includes and configures the `@ember-data/json-api` cache for you. +> **Note** +> The `ember-data` package automatically includes and configures the `@ember-data/json-api` cache for you. ### Handling Requests @@ -89,7 +99,8 @@ When *Ember***Data** needs to fetch or save data it will pass that request to yo To start, let's install the `RequestManager` from `@ember-data/request` and the basic `Fetch` handler from ``@ember-data/request/fetch`. -> **Note** If your app uses `GraphQL`, `REST` or different conventions for `JSON:API` than your cache expects, other handlers may better fit your data. You can author your own handler by creating one that conforms to the [handler interface](https://github.com/emberjs/data/tree/main/packages/request#handling-requests). +> **Note** +> If your app uses `GraphQL`, `REST` or different conventions for `JSON:API` than your cache expects, other handlers may better fit your data. You can author your own handler by creating one that conforms to the [handler interface](https://github.com/emberjs/data/tree/main/packages/request#handling-requests). ```ts import Store, { CacheHandler } from '@ember-data/store'; @@ -185,5 +196,6 @@ Typically you will choose an existing record implementation such as `@ember-data Because of the boundaries around instantiation and the cache, record implementations should be capable of interop both with each other and with any `Cache`. Due to this, if needed an application can utilize multiple record implementations and multiple cache implementations either to support enhanced features for only a subset of records or to be able to incrementally migrate from one record/cache to another record or cache. -> **Note:** The `ember-data` package automatically includes the `@ember-data/model` +> **Note** +> The `ember-data` package automatically includes the `@ember-data/model` > package and configures it for you. diff --git a/packages/store/addon-main.cjs b/packages/store/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/store/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/store/addon-main.js b/packages/store/addon-main.js deleted file mode 100644 index 13f812d930a..00000000000 --- a/packages/store/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES, ownConfig.features || {}, hostOptions.features || {}); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/store/babel.config.js b/packages/store/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/store/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/store/babel.config.mjs b/packages/store/babel.config.mjs new file mode 100644 index 00000000000..c23b859273f --- /dev/null +++ b/packages/store/babel.config.mjs @@ -0,0 +1,12 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ['module:decorator-transforms', { runtime: { import: 'decorator-transforms/runtime' } }], + ], +}; diff --git a/packages/store/eslint.config.mjs b/packages/store/eslint.config.mjs new file mode 100644 index 00000000000..925c8234e7c --- /dev/null +++ b/packages/store/eslint.config.mjs @@ -0,0 +1,29 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import { externals } from './vite.config.mjs'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + js.browser({ + srcDirs: ['src'], + allowedImports: externals, + }), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: externals, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/store/package.json b/packages/store/package.json index 589d105c29e..0898da76cd9 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -11,90 +11,91 @@ "directory": "packages/store" }, "license": "MIT", - "author": "", - "directories": {}, - "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" - }, - "ember-addon": { - "main": "addon-main.js", - "type": "addon", - "version": 1 - }, + "author": "Chris Thoburn ", "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", - "ember-data-logo-light.svg", - "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], - "peerDependencies": { - "@ember-data/model": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "@glimmer/tracking": "^1.1.2" - }, - "peerDependenciesMeta": { - "@ember-data/json-api": { - "optional": true - }, - "@ember-data/graph": { - "optional": true - }, - "@ember-data/legacy-compat": { - "optional": true + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" }, - "@ember-data/model": { - "optional": true + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" } }, + "scripts": { + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { "injected": true } }, "dependencies": { - "ember-cli-babel": "^7.26.11", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0", - "ember-cached-decorator-polyfill": "^1.0.1" + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/core-types": "workspace:*" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/tracking": "workspace:*", "@glimmer/component": "^1.1.2", - "ember-source": "~4.12.0", - "@embroider/addon-dev": "^3.0.0", - "rollup": "^3.20.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/preset-env": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "tslib": "^2.5.0", - "walk-sync": "^3.0.0", - "typescript": "^5.0.3", - "webpack": "^5.77.0" + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "decorator-transforms": "^2.2.2", + "ember-source": "~5.12.0", + "expect-type": "^0.20.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "ember-addon": { + "main": "addon-main.cjs", + "type": "addon", + "version": 2 + }, + "ember": { + "edition": "octane" + } } diff --git a/packages/store/rollup.config.mjs b/packages/store/rollup.config.mjs deleted file mode 100644 index a60cad7b9b4..00000000000 --- a/packages/store/rollup.config.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: [ - '@embroider/macros', - '@glimmer/tracking', - - '@ember-data/legacy-compat/-private', - '@ember-data/tracking/-private', - '@ember-data/private-build-infra/current-deprecations', - '@ember/-internals/glimmer', - '@ember/-internals/metal', - '@glimmer/validator', - - // to eliminate - '@ember/object/compat', - '@ember/runloop', - '@ember/string', - 'ember', - - // investigate why these are present - '@ember/application', - '@ember/object/computed', - - // deprecated usages only - '@ember/object', - '@ember/object/proxy', - '@ember/object/promise-proxy-mixin', - '@ember/array/proxy', - - // test/debug only - '@ember/test', - '@ember/debug', - ], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', '-private.js']), - - nodeResolve({ extensions: ['.ts', '.js'] }), - babel({ - extensions: ['.ts', '.js'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/store/src/-private.ts b/packages/store/src/-private.ts index 3a50ff42290..9100d7b2a94 100644 --- a/packages/store/src/-private.ts +++ b/packages/store/src/-private.ts @@ -1 +1,84 @@ -export * from './-private/index'; +/** + @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'; + +export { recordIdentifierFor } from './-private/caches/instance-cache'; + +export { CacheHandler, type StoreRequestContext } from './-private/cache-handler/handler'; +export { type CachePolicy } from './-private/cache-handler/types'; + +export { isStableIdentifier } from './-private/caches/identifier-cache'; + +export { constructResource } from './-private/utils/construct-resource'; + +export type { Document } from './-private/document'; +export type { InstanceCache } from './-private/caches/instance-cache'; + +export type { + FindRecordQuery, + Request, + SaveRecordMutation, + RequestState, + RequestStateService, +} from './-private/network/request-cache'; + +export type { CreateRecordProperties } from './-private/store-service'; + +// TODO this should be a deprecated helper but we have so much usage of it +// to also eliminate +export { coerceId, ensureStringId } from './-private/utils/coerce-id'; +export type { NativeProxy } from './-private/record-arrays/native-proxy-type-fix'; +export { + IdentifierArray as LiveArray, + Collection as CollectionRecordArray, + notifyArray, + SOURCE, + MUTATE, + ARRAY_SIGNAL, +} from './-private/record-arrays/identifier-array'; +export { RecordArrayManager, fastPush } from './-private/managers/record-array-manager'; + +// leaked for private use / test use, should investigate removing +export { _clearCaches } from './-private/caches/instance-cache'; +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 type { StoreRequestInput } from './-private/cache-handler/handler'; + +/** + 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.ts b/packages/store/src/-private/cache-handler.ts deleted file mode 100644 index c594dd042cc..00000000000 --- a/packages/store/src/-private/cache-handler.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { assert } from '@ember/debug'; - -import type { - CacheHandler as CacheHandlerType, - Future, - ImmutableRequestInfo, - NextFn, - RequestContext, - ResponseInfo, - StructuredDataDocument, - StructuredErrorDocument, -} from '@ember-data/request/-private/types'; -import type Store from '@ember-data/store'; -import { - CollectionResourceDataDocument, - ResourceDataDocument, - ResourceErrorDocument, -} from '@ember-data/types/cache/document'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiError } from '@ember-data/types/q/record-data-json-api'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; - -import { Document } from './document'; - -/** - * A service which an application may provide to the store via - * the store's `lifetimes` property to configure the behavior - * of the CacheHandler. - * - * The default behavior for request lifetimes is to never expire - * unless manually refreshed via `cacheOptions.reload` or `cacheOptions.backgroundReload`. - * - * Implementing this service allows you to programatically define - * when a request should be considered expired. - * - * @class LifetimesService - * @public - */ -export interface LifetimesService { - /** - * Invoked to determine if the request may be fulfilled from cache - * if possible. - * - * Note, this is only invoked if the request has a cache-key. - * - * If no cache entry is found or the entry is hard expired, - * the request will be fulfilled from the configured request handlers - * and the cache will be updated before returning the response. - * - * @method isHardExpired - * @public - * @param {StableDocumentIdentifier} identifier - * @param {Store} store - * @return {boolean} true if the request is considered hard expired - */ - isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean; - /** - * Invoked if `isHardExpired` is false to determine if the request - * should be update behind the scenes if cache data is already available. - * - * Note, this is only invoked if the request has a cache-key. - * - * If true, the request will be fulfilled from cache while a backgrounded - * request is made to update the cache via the configured request handlers. - * - * @method isSoftExpired - * @public - * @param {StableDocumentIdentifier} identifier - * @param {Store} store - * @return {boolean} true if the request is considered soft expired - */ - isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean; - - /** - * Invoked when a request will be sent to the configured request handlers. - * This is invoked for both foreground and background requests. - * - * Note, this is invoked regardless of whether the request has a cache-key. - * - * @method willRequest [Optional] - * @public - * @param {ImmutableRequestInfo} request - * @param {StableDocumentIdentifier | null} identifier - * @param {Store} store - * @return {void} - */ - willRequest?(request: ImmutableRequestInfo, identifier: StableDocumentIdentifier | null, store: Store): void; - - /** - * Invoked when a request has been fulfilled from the configured request handlers. - * This is invoked for both foreground and background requests once the cache has - * been updated. - * - * Note, this is invoked regardless of whether the request has a cache-key. - * - * @method didRequest [Optional] - * @public - * @param {ImmutableRequestInfo} request - * @param {ImmutableResponse} response - * @param {StableDocumentIdentifier | null} identifier - * @param {Store} store - * @return {void} - */ - didRequest?( - request: ImmutableRequestInfo, - response: Response | ResponseInfo | null, - identifier: StableDocumentIdentifier | null, - store: Store - ): void; -} - -export type LooseStoreRequestInfo = Omit & { - records?: ResourceIdentifierObject[]; - headers?: Headers; -}; - -export type StoreRequestInput = ImmutableRequestInfo | LooseStoreRequestInfo; - -export interface StoreRequestContext extends RequestContext { - request: ImmutableRequestInfo & { store: Store; [EnableHydration]?: boolean }; -} - -const MUTATION_OPS = new Set(['createRecord', 'updateRecord', 'deleteRecord']); - -function isErrorDocument(document: ResourceDataDocument | ResourceErrorDocument): document is ResourceErrorDocument { - return 'errors' in document; -} - -function maybeUpdateUiObjects( - store: Store, - request: ImmutableRequestInfo, - options: { - shouldHydrate?: boolean; - shouldFetch?: boolean; - shouldBackgroundFetch?: boolean; - identifier: StableDocumentIdentifier | null; - }, - document: ResourceDataDocument | ResourceErrorDocument | null, - isFromCache: boolean -): T { - const { identifier } = options; - - if (!document) { - assert(`The CacheHandler expected response content but none was found`, !options.shouldHydrate); - return document as T; - } - - if (isErrorDocument(document)) { - if (!identifier && !options.shouldHydrate) { - return document as T; - } - let doc: Document | undefined; - if (identifier) { - doc = store._documentCache.get(identifier) as Document | undefined; - } - - if (!doc) { - doc = new Document(store, identifier); - copyDocumentProperties(doc, document); - - if (identifier) { - store._documentCache.set(identifier, doc); - } - } else if (!isFromCache) { - doc.data = undefined; - copyDocumentProperties(doc, document); - } - - return options.shouldHydrate ? (doc as T) : (document as T); - } - - if (Array.isArray(document.data)) { - const { recordArrayManager } = store; - if (!identifier) { - if (!options.shouldHydrate) { - return document as T; - } - const data = recordArrayManager.createArray({ - type: request.url as string, - identifiers: document.data, - doc: document as CollectionResourceDataDocument, - query: request, - }) as T; - - const doc = new Document(store, null); - doc.data = data; - doc.meta = document.meta!; - doc.links = document.links!; - - return doc as T; - } - let managed = recordArrayManager._keyedArrays.get(identifier.lid); - - if (!managed) { - managed = recordArrayManager.createArray({ - type: identifier.lid, - identifiers: document.data, - doc: document as CollectionResourceDataDocument, - }); - recordArrayManager._keyedArrays.set(identifier.lid, managed); - const doc = new Document(store, identifier); - doc.data = managed; - doc.meta = document.meta!; - doc.links = document.links!; - store._documentCache.set(identifier, doc); - - return options.shouldHydrate ? (doc as T) : (document as T); - } else { - const doc = store._documentCache.get(identifier)!; - if (!isFromCache) { - recordArrayManager.populateManagedArray(managed, document.data, document as CollectionResourceDataDocument); - doc.data = managed; - doc.meta = document.meta!; - doc.links = document.links!; - } - - return options.shouldHydrate ? (doc as T) : (document as T); - } - } else { - if (!identifier && !options.shouldHydrate) { - return document as T; - } - const data = document.data ? store.peekRecord(document.data) : null; - let doc: Document | undefined; - if (identifier) { - doc = store._documentCache.get(identifier) as Document | undefined; - } - - if (!doc) { - doc = new Document(store, identifier); - doc.data = data; - copyDocumentProperties(doc, document); - - if (identifier) { - store._documentCache.set(identifier, doc); - } - } else if (!isFromCache) { - doc.data = data; - copyDocumentProperties(doc, document); - } - - return options.shouldHydrate ? (doc as T) : (document as T); - } -} - -function calcShouldFetch( - store: Store, - request: ImmutableRequestInfo, - hasCachedValue: boolean, - identifier: StableDocumentIdentifier | null -): boolean { - const { cacheOptions } = request; - return ( - (request.op && MUTATION_OPS.has(request.op)) || - cacheOptions?.reload || - !hasCachedValue || - (store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier, store) : false) - ); -} - -function calcShouldBackgroundFetch( - store: Store, - request: ImmutableRequestInfo, - willFetch: boolean, - identifier: StableDocumentIdentifier | null -): boolean { - const { cacheOptions } = request; - return ( - !willFetch && - (cacheOptions?.backgroundReload || - (store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier, store) : false)) - ); -} - -function isMutation( - request: Partial -): request is ImmutableRequestInfo & { op: 'createRecord' | 'updateRecord' | 'deleteRecord' } { - return Boolean(request.op && MUTATION_OPS.has(request.op)); -} - -function fetchContentAndHydrate( - next: NextFn, - context: StoreRequestContext, - identifier: StableDocumentIdentifier | null, - shouldFetch: boolean, - shouldBackgroundFetch: boolean -): Promise { - const { store } = context.request; - const shouldHydrate: boolean = context.request[EnableHydration] || false; - - let isMut = false; - if (isMutation(context.request)) { - isMut = true; - // TODO should we handle multiple records in request.records by iteratively calling willCommit for each - const record = context.request.data?.record || context.request.records?.[0]; - assert( - `Expected to receive a list of records included in the ${context.request.op} request`, - record || !shouldHydrate - ); - if (record) { - store.cache.willCommit(record as StableRecordIdentifier, context); - } - } - - if (store.lifetimes?.willRequest) { - store.lifetimes.willRequest(context.request, identifier, store); - } - - const promise = next(context.request).then( - (document) => { - store.requestManager._pending.delete(context.id); - store._enableAsyncFlush = true; - let response: ResourceDataDocument; - store._join(() => { - if (isMutation(context.request)) { - const record = context.request.data?.record || context.request.records?.[0]; - if (record) { - response = store.cache.didCommit(record as StableRecordIdentifier, document) as ResourceDataDocument; - - // a mutation combined with a 204 has no cache impact when no known records were involved - // a createRecord with a 201 with an empty response and no known records should similarly - // have no cache impact - } else if (isCacheAffecting(document)) { - response = store.cache.put(document) as ResourceDataDocument; - } - } else { - response = store.cache.put(document) as ResourceDataDocument; - } - response = maybeUpdateUiObjects( - store, - context.request, - { shouldHydrate, shouldFetch, shouldBackgroundFetch, identifier }, - response, - false - ); - }); - store._enableAsyncFlush = null; - - if (store.lifetimes?.didRequest) { - store.lifetimes.didRequest(context.request, document.response, identifier, store); - } - - if (shouldFetch) { - return response!; - } else if (shouldBackgroundFetch) { - store.notifications._flush(); - } - }, - (error: StructuredErrorDocument) => { - store.requestManager._pending.delete(context.id); - if (context.request.signal?.aborted) { - throw error; - } - store.requestManager._pending.delete(context.id); - store._enableAsyncFlush = true; - let response: ResourceErrorDocument | undefined; - store._join(() => { - if (isMutation(context.request)) { - // TODO similar to didCommit we should spec this to be similar to cache.put for handling full response - // currently we let the response remain undefiend. - const errors = - error && - error.content && - typeof error.content === 'object' && - 'errors' in error.content && - Array.isArray(error.content.errors) - ? (error.content.errors as JsonApiError[]) - : undefined; - - const record = context.request.data?.record || context.request.records?.[0]; - - store.cache.commitWasRejected(record as StableRecordIdentifier, errors); - // re-throw the original error to preserve `errors` property. - throw error; - } else { - response = store.cache.put(error) as ResourceErrorDocument; - response = maybeUpdateUiObjects( - store, - context.request, - { shouldHydrate, shouldFetch, shouldBackgroundFetch, identifier }, - response, - false - ); - } - }); - store._enableAsyncFlush = null; - - if (identifier && store.lifetimes?.didRequest) { - store.lifetimes.didRequest(context.request, error.response, identifier, store); - } - - if (!shouldBackgroundFetch) { - const newError = cloneError(error); - newError.content = response!; - throw newError; - } else { - store.notifications._flush(); - } - } - ) as Promise; - - if (!isMut) { - return promise; - } - assert(`Expected a mutation`, isMutation(context.request)); - - // for mutations we need to enqueue the promise with the requestStateService - // TODO should we enque a request per record in records? - const record = context.request.data?.record || context.request.records?.[0]; - - return store._requestCache._enqueue(promise, { - data: [{ op: 'saveRecord', recordIdentifier: record as StableRecordIdentifier, options: undefined }], - }); -} - -function isAggregateError( - error: Error & { errors?: JsonApiError[] } -): error is AggregateError & { errors: JsonApiError[] } { - return error instanceof AggregateError || (error.name === 'AggregateError' && Array.isArray(error.errors)); -} - -type RobustError = Error & { error: string | object; errors?: JsonApiError[]; content?: unknown }; - -// TODO @runspired, consider if we should deep freeze errors (potentially only in debug) vs cloning them -function cloneError(error: RobustError) { - const isAggregate = isAggregateError(error); - - const cloned = ( - isAggregate - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - new AggregateError(structuredClone(error.errors as JsonApiError[]), error.message) - : new Error(error.message) - ) as RobustError; - cloned.stack = error.stack!; - cloned.error = error.error; - - // copy over enumerable properties - Object.assign(cloned, error); - - return cloned; -} - -export const SkipCache = Symbol.for('ember-data:skip-cache'); -export const EnableHydration = Symbol.for('ember-data:enable-hydration'); - -/** - * A CacheHandler that adds support for using an EmberData Cache with a RequestManager. - * - * This handler will only run when a request has supplied a `store` instance. Requests - * issued by the store via `store.request()` will automatically have the `store` instance - * attached to the request. - * - * ```ts - * requestManager.request({ - * store: store, - * url: '/api/posts', - * method: 'GET' - * }); - * ``` - * - * When this handler elects to handle a request, it will return the raw `StructuredDocument` - * unless the request has `[EnableHydration]` set to `true`. In this case, the handler will - * return a `Document` instance that will automatically update the UI when the cache is updated - * in the future and will hydrate any identifiers in the StructuredDocument into Record instances. - * - * When issuing a request via the store, [EnableHydration] is automatically set to `true`. This - * means that if desired you can issue requests that utilize the cache without needing to also - * utilize Record instances if desired. - * - * Said differently, you could elect to issue all requests via a RequestManager, without ever using - * the store directly, by setting [EnableHydration] to `true` and providing a store instance. Not - * necessarily the most useful thing, but the decoupled nature of the RequestManager and incremental-feature - * approach of EmberData allows for this flexibility. - * - * ```ts - * import { EnableHydration } from '@warp-drive/core-types/request'; - * - * requestManager.request({ - * store: store, - * url: '/api/posts', - * method: 'GET', - * [EnableHydration]: true - * }); - * - * @typedoc - */ -export const CacheHandler: CacheHandlerType = { - request(context: StoreRequestContext, next: NextFn): Promise> | Future | T { - // if we have no cache or no cache-key skip cache handling - if (!context.request.store || context.request.cacheOptions?.[SkipCache]) { - return next(context.request); - } - - const { store } = context.request; - const identifier = store.identifierCache.getOrCreateDocumentIdentifier(context.request); - - const peeked = identifier ? store.cache.peekRequest(identifier) : null; - - // determine if we should skip cache - if (calcShouldFetch(store, context.request, !!peeked, identifier)) { - return fetchContentAndHydrate(next, context, identifier, true, false); - } - - // if we have not skipped cache, determine if we should update behind the scenes - if (calcShouldBackgroundFetch(store, context.request, false, identifier)) { - const promise = fetchContentAndHydrate(next, context, identifier, false, true); - store.requestManager._pending.set(context.id, promise); - } - - const shouldHydrate: boolean = context.request[EnableHydration] || false; - - if ('error' in peeked!) { - const content = shouldHydrate - ? maybeUpdateUiObjects( - store, - context.request, - { shouldHydrate, identifier }, - peeked.content as ResourceErrorDocument, - true - ) - : peeked.content; - const newError = cloneError(peeked); - newError.content = content as object; - throw newError; - } - - const result = shouldHydrate - ? maybeUpdateUiObjects( - store, - context.request, - { shouldHydrate, identifier }, - peeked!.content as ResourceDataDocument, - true - ) - : (peeked!.content as T); - - return result; - }, -}; - -function copyDocumentProperties(target: { links?: unknown; meta?: unknown; errors?: unknown }, source: object) { - if ('links' in source) { - target.links = source.links; - } - if ('meta' in source) { - target.meta = source.meta; - } - if ('errors' in source) { - target.errors = source.errors; - } -} - -function isCacheAffecting(document: StructuredDataDocument): boolean { - if (!isMutation(document.request)) { - return true; - } - // a mutation combined with a 204 has no cache impact when no known records were involved - // a createRecord with a 201 with an empty response and no known records should similarly - // have no cache impact - - if (document.request.op === 'createRecord' && document.response?.status === 201) { - return document.content ? Object.keys(document.content).length > 0 : false; - } - - return document.response?.status !== 204; -} diff --git a/packages/store/src/-private/cache-handler/handler.ts b/packages/store/src/-private/cache-handler/handler.ts new file mode 100644 index 00000000000..6ec9006e5e7 --- /dev/null +++ b/packages/store/src/-private/cache-handler/handler.ts @@ -0,0 +1,471 @@ +/** + * @module @ember-data/store + */ +import type { CacheHandler as CacheHandlerType, Future, NextFn } from '@ember-data/request'; +import type { ManagedRequestPriority } from '@ember-data/request/-private/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { + ImmutableRequestInfo, + RequestContext, + StructuredDataDocument, + StructuredErrorDocument, +} from '@warp-drive/core-types/request'; +import { EnableHydration, SkipCache } from '@warp-drive/core-types/request'; +import type { + CollectionResourceDataDocument, + ResourceDataDocument, + ResourceErrorDocument, +} from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; + +import { Document } from '../document'; +import type { Store } from '../store-service'; +import { + calcShouldBackgroundFetch, + calcShouldFetch, + cloneError, + copyDocumentProperties, + getPriority, + isCacheAffecting, + isErrorDocument, + isMutation, +} from './utils'; + +export type LooseStoreRequestInfo = Omit< + ImmutableRequestInfo, + 'records' | 'headers' +> & { + records?: ResourceIdentifierObject[]; + headers?: Headers; +}; + +export type StoreRequestInput = ImmutableRequestInfo | LooseStoreRequestInfo; + +export interface StoreRequestContext extends RequestContext { + request: ImmutableRequestInfo & { store: Store; [EnableHydration]?: boolean }; +} + +/** + * A CacheHandler that adds support for using an EmberData Cache with a RequestManager. + * + * This handler will only run when a request has supplied a `store` instance. Requests + * issued by the store via `store.request()` will automatically have the `store` instance + * attached to the request. + * + * ```ts + * requestManager.request({ + * store: store, + * url: '/api/posts', + * method: 'GET' + * }); + * ``` + * + * When this handler elects to handle a request, it will return the raw `StructuredDocument` + * unless the request has `[EnableHydration]` set to `true`. In this case, the handler will + * return a `Document` instance that will automatically update the UI when the cache is updated + * in the future and will hydrate any identifiers in the StructuredDocument into Record instances. + * + * When issuing a request via the store, [EnableHydration] is automatically set to `true`. This + * means that if desired you can issue requests that utilize the cache without needing to also + * utilize Record instances if desired. + * + * Said differently, you could elect to issue all requests via a RequestManager, without ever using + * the store directly, by setting [EnableHydration] to `true` and providing a store instance. Not + * necessarily the most useful thing, but the decoupled nature of the RequestManager and incremental-feature + * approach of EmberData allows for this flexibility. + * + * ```ts + * import { EnableHydration } from '@warp-drive/core-types/request'; + * + * requestManager.request({ + * store: store, + * url: '/api/posts', + * method: 'GET', + * [EnableHydration]: true + * }); + * + * @typedoc + */ +export const CacheHandler: CacheHandlerType = { + request( + context: StoreRequestContext & { setIdentifier(identifier: StableDocumentIdentifier): void }, + next: NextFn + ): Promise> | Future | T { + // if we have no cache or no cache-key skip cache handling + if (!context.request.store || context.request.cacheOptions?.[SkipCache]) { + return next(context.request); + } + + const { store } = context.request; + const identifier = store.identifierCache.getOrCreateDocumentIdentifier(context.request); + + if (identifier) { + context.setIdentifier(identifier); + } + + // used to dedupe existing requests that match + const DEDUPE = store.requestManager._deduped; + const activeRequest = identifier && DEDUPE.get(identifier); + const peeked = identifier ? store.cache.peekRequest(identifier) : null; + + // determine if we should skip cache + if (calcShouldFetch(store, context.request, !!peeked, identifier)) { + if (activeRequest) { + activeRequest.priority = { blocking: true }; + return activeRequest.promise as Promise; + } + let promise = fetchContentAndHydrate(next, context, identifier, { blocking: true }); + if (identifier) { + promise = promise.finally(() => { + DEDUPE.delete(identifier); + store.notifications.notify(identifier, 'state'); + }); + DEDUPE.set(identifier, { priority: { blocking: true }, promise }); + store.notifications.notify(identifier, 'state'); + } + return promise; + } + + // if we have not skipped cache, determine if we should update behind the scenes + if (calcShouldBackgroundFetch(store, context.request, false, identifier)) { + let promise = activeRequest?.promise || fetchContentAndHydrate(next, context, identifier, { blocking: false }); + if (identifier && !activeRequest) { + promise = promise.finally(() => { + DEDUPE.delete(identifier); + store.notifications.notify(identifier, 'state'); + }); + DEDUPE.set(identifier, { priority: { blocking: false }, promise }); + store.notifications.notify(identifier, 'state'); + } + store.requestManager._pending.set(context.id, promise); + } + + assert(`Expected a peeked request to be present`, peeked); + + const shouldHydrate: boolean = context.request[EnableHydration] || false; + context.setResponse(peeked.response); + + if ('error' in peeked) { + const content = shouldHydrate + ? maybeUpdateErrorUiObjects( + store, + { shouldHydrate, identifier }, + peeked.content as ResourceErrorDocument, + true + ) + : peeked.content; + const newError = cloneError(peeked); + newError.content = content as object; + throw newError; + } + + const result = shouldHydrate + ? (maybeUpdateUiObjects( + store, + context.request, + { shouldHydrate, identifier }, + peeked.content as ResourceDataDocument, + true + ) as T) + : (peeked.content as T); + + return result; + }, +}; + +type HydrationOptions = { + shouldHydrate?: boolean; + identifier: StableDocumentIdentifier | null; +}; + +type UpdateOptions = HydrationOptions & { + priority: ManagedRequestPriority; +}; + +function maybeUpdateUiObjects( + store: Store, + request: ImmutableRequestInfo, + options: HydrationOptions, + document: ResourceDataDocument | null, + isFromCache: boolean +): Document | ResourceDataDocument | null { + const { identifier } = options; + + if (!document) { + assert(`The CacheHandler expected response content but none was found`, !options.shouldHydrate); + return document; + } + + if (Array.isArray(document.data)) { + const { recordArrayManager } = store; + if (!identifier) { + if (!options.shouldHydrate) { + return document; + } + const data = recordArrayManager.createArray({ + type: request.url as string, + identifiers: document.data, + doc: document as CollectionResourceDataDocument, + query: request, + }) as T; + + const doc = new Document(store, null); + doc.data = data; + doc.meta = document.meta!; + doc.links = document.links!; + + return doc; + } + let managed = recordArrayManager._keyedArrays.get(identifier.lid); + + if (!managed) { + managed = recordArrayManager.createArray({ + type: identifier.lid, + identifiers: document.data, + doc: document as CollectionResourceDataDocument, + }); + recordArrayManager._keyedArrays.set(identifier.lid, managed); + const doc = new Document(store, identifier); + doc.data = managed as T; + doc.meta = document.meta!; + doc.links = document.links!; + store._documentCache.set(identifier, doc); + + return options.shouldHydrate ? doc : document; + } else { + const doc = store._documentCache.get(identifier) as Document; + if (!isFromCache) { + recordArrayManager.populateManagedArray(managed, document.data, document as CollectionResourceDataDocument); + doc.data = managed as T; + doc.meta = document.meta!; + doc.links = document.links!; + } + + return options.shouldHydrate ? doc : document; + } + } else { + if (!identifier && !options.shouldHydrate) { + return document; + } + const data = (document.data ? store.peekRecord(document.data) : null) as T; + let doc: Document | undefined; + if (identifier) { + doc = store._documentCache.get(identifier) as Document | undefined; + } + + if (!doc) { + doc = new Document(store, identifier); + doc.data = data; + copyDocumentProperties(doc, document); + + if (identifier) { + store._documentCache.set(identifier, doc); + } + } else if (!isFromCache) { + doc.data = data; + copyDocumentProperties(doc, document); + } + + return options.shouldHydrate ? doc : document; + } +} + +function maybeUpdateErrorUiObjects( + store: Store, + options: HydrationOptions, + document: ResourceErrorDocument, + isFromCache: boolean +): ResourceErrorDocument { + const { identifier } = options; + + // TODO investigate why ResourceErrorDocument is insufficient for expressing all error types + if (!isErrorDocument(document) || (!identifier && !options.shouldHydrate)) { + return document; + } + + let doc: Document | undefined; + if (identifier) { + doc = store._documentCache.get(identifier) as Document | undefined; + } + + if (!doc) { + doc = new Document(store, identifier); + copyDocumentProperties(doc, document); + + if (identifier) { + store._documentCache.set(identifier, doc); + } + } else if (!isFromCache) { + doc.data = undefined; + copyDocumentProperties(doc, document); + } + + return options.shouldHydrate ? (doc as ResourceErrorDocument) : document; +} + +function updateCacheForSuccess( + store: Store, + request: StoreRequestContext['request'], + options: HydrationOptions, + document: StructuredDataDocument +) { + let response: ResourceDataDocument | null = null; + if (isMutation(request)) { + const record = request.data?.record || request.records?.[0]; + if (record) { + response = store.cache.didCommit(record, document) as ResourceDataDocument; + + // a mutation combined with a 204 has no cache impact when no known records were involved + // a createRecord with a 201 with an empty response and no known records should similarly + // have no cache impact + } else if (isCacheAffecting(document)) { + response = store.cache.put(document) as ResourceDataDocument; + } + } else { + response = store.cache.put(document) as ResourceDataDocument; + } + return maybeUpdateUiObjects(store, request, options, response, false); +} + +function handleFetchSuccess( + store: Store, + context: StoreRequestContext, + options: UpdateOptions, + document: StructuredDataDocument +): ResourceDataDocument | void { + const { request } = context; + store.requestManager._pending.delete(context.id); + store._enableAsyncFlush = true; + let response: ResourceDataDocument; + store._join(() => { + response = updateCacheForSuccess(store, request, options, document) as ResourceDataDocument; + }); + store._enableAsyncFlush = null; + + if (store.lifetimes?.didRequest) { + store.lifetimes.didRequest(context.request, document.response, options.identifier, store); + } + + const finalPriority = getPriority(options.identifier, store.requestManager._deduped, options.priority); + if (finalPriority.blocking) { + return response!; + } else { + store.notifications._flush(); + } +} + +function updateCacheForError( + store: Store, + context: StoreRequestContext, + options: HydrationOptions, + error: StructuredErrorDocument +) { + let response: ResourceErrorDocument | undefined; + if (isMutation(context.request)) { + // TODO similar to didCommit we should spec this to be similar to cache.put for handling full response + // currently we let the response remain undefiend. + const errors = + error && + error.content && + typeof error.content === 'object' && + 'errors' in error.content && + Array.isArray(error.content.errors) + ? (error.content.errors as ApiError[]) + : undefined; + + const record = context.request.data?.record || context.request.records?.[0]; + + store.cache.commitWasRejected(record, errors); + } else { + response = store.cache.put(error) as ResourceErrorDocument; + return maybeUpdateErrorUiObjects(store, options, response, false); + } +} + +function handleFetchError( + store: Store, + context: StoreRequestContext, + options: UpdateOptions, + error: StructuredErrorDocument +): ResourceErrorDocument | void { + store.requestManager._pending.delete(context.id); + if (context.request.signal?.aborted) { + throw error; + } + store._enableAsyncFlush = true; + let response: ResourceErrorDocument | undefined; + store._join(() => { + response = updateCacheForError(store, context, options, error); + }); + store._enableAsyncFlush = null; + + if (options.identifier && store.lifetimes?.didRequest) { + store.lifetimes.didRequest(context.request, error.response, options.identifier, store); + } + + if (isMutation(context.request)) { + throw error; + } + + const finalPriority = getPriority(options.identifier, store.requestManager._deduped, options.priority); + if (finalPriority.blocking) { + const newError = cloneError(error); + newError.content = response!; + throw newError; + } else { + store.notifications._flush(); + } +} + +function fetchContentAndHydrate( + next: NextFn, + context: StoreRequestContext, + identifier: StableDocumentIdentifier | null, + priority: { blocking: boolean } +): Promise { + const { store } = context.request; + const shouldHydrate: boolean = context.request[EnableHydration] || false; + const options = { shouldHydrate, identifier, priority }; + + let isMut = false; + if (isMutation(context.request)) { + isMut = true; + // TODO should we handle multiple records in request.records by iteratively calling willCommit for each + const record = context.request.data?.record || context.request.records?.[0]; + assert( + `Expected to receive a list of records included in the ${context.request.op} request`, + record || !shouldHydrate + ); + if (record) { + store.cache.willCommit(record, context); + } + } + + if (store.lifetimes?.willRequest) { + store.lifetimes.willRequest(context.request, identifier, store); + } + + const promise = next(context.request).then( + (document) => { + return handleFetchSuccess(store, context, options, document); + }, + (error: StructuredErrorDocument) => { + return handleFetchError(store, context, options, error); + } + ) as Promise; + + if (!isMut) { + return promise; + } + assert(`Expected a mutation`, isMutation(context.request)); + + // for mutations we need to enqueue the promise with the requestStateService + // TODO should we enque a request per record in records? + const record = context.request.data?.record || context.request.records?.[0]; + + return store._requestCache._enqueue(promise, { + data: [{ op: 'saveRecord', recordIdentifier: record, options: undefined }], + }); +} diff --git a/packages/store/src/-private/cache-handler/types.ts b/packages/store/src/-private/cache-handler/types.ts new file mode 100644 index 00000000000..22f5acc60d3 --- /dev/null +++ b/packages/store/src/-private/cache-handler/types.ts @@ -0,0 +1,113 @@ +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { ImmutableRequestInfo, ResponseInfo } from '@warp-drive/core-types/request'; + +import type { Store } from '../store-service'; + +/** + * A service which an application may provide to the store via + * the store's `lifetimes` property to configure the behavior + * of the CacheHandler. + * + * The default behavior for request lifetimes is to never expire + * unless manually refreshed via `cacheOptions.reload` or `cacheOptions.backgroundReload`. + * + * Implementing this service allows you to programatically define + * when a request should be considered expired. + * + * @class CachePolicy + * @public + */ +export interface CachePolicy { + /** + * Invoked to determine if the request may be fulfilled from cache + * if possible. + * + * Note, this is only invoked if the request has a cache-key. + * + * If no cache entry is found or the entry is hard expired, + * the request will be fulfilled from the configured request handlers + * and the cache will be updated before returning the response. + * + * @method isHardExpired + * @public + * @param {StableDocumentIdentifier} identifier + * @param {Store} store + * @return {boolean} true if the request is considered hard expired + */ + isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean; + /** + * Invoked if `isHardExpired` is false to determine if the request + * should be update behind the scenes if cache data is already available. + * + * Note, this is only invoked if the request has a cache-key. + * + * If true, the request will be fulfilled from cache while a backgrounded + * request is made to update the cache via the configured request handlers. + * + * @method isSoftExpired + * @public + * @param {StableDocumentIdentifier} identifier + * @param {Store} store + * @return {boolean} true if the request is considered soft expired + */ + isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean; + + /** + * Invoked when a request will be sent to the configured request handlers. + * This is invoked for both foreground and background requests. + * + * Note, this is invoked regardless of whether the request has a cache-key. + * + * @method willRequest [Optional] + * @public + * @param {ImmutableRequestInfo} request + * @param {StableDocumentIdentifier | null} identifier + * @param {Store} store + * @return {void} + */ + willRequest?(request: ImmutableRequestInfo, identifier: StableDocumentIdentifier | null, store: Store): void; + + /** + * Invoked when a request has been fulfilled from the configured request handlers. + * This is invoked for both foreground and background requests once the cache has + * been updated. + * + * Note, this is invoked regardless of whether the request has a cache-key. + * + * It is best practice to notify the store of any requests marked as invalidated + * so that request subscriptions can reload when needed. + * + * ```ts + * store.notifications.notify(identifier, 'invalidated'); + * ``` + * + * This allows anything subscribed to the request to be notified of the change + * + * e.g. + * + * ```ts + * store.notifications.subscribe(identifier, (_, type) => { + * if (type === 'invalidated') { + * // do update + * } + * }); + * ``` + * + * Note, + * + * + * @method didRequest [Optional] + * @public + * @param {ImmutableRequestInfo} request + * @param {ImmutableResponse} response + * @param {StableDocumentIdentifier | null} identifier + * @param {Store} store + * @return {void} + */ + didRequest?( + request: ImmutableRequestInfo, + response: Response | ResponseInfo | null, + identifier: StableDocumentIdentifier | null, + store: Store + ): void; +} diff --git a/packages/store/src/-private/cache-handler/utils.ts b/packages/store/src/-private/cache-handler/utils.ts new file mode 100644 index 00000000000..094733a61c2 --- /dev/null +++ b/packages/store/src/-private/cache-handler/utils.ts @@ -0,0 +1,120 @@ +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { + ImmutableCreateRequestOptions, + ImmutableDeleteRequestOptions, + ImmutableRequestInfo, + ImmutableUpdateRequestOptions, + StructuredDataDocument, +} from '@warp-drive/core-types/request'; +import type { ResourceDataDocument, ResourceErrorDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; + +import type { Store } from '../store-service'; + +export const MUTATION_OPS = new Set(['createRecord', 'updateRecord', 'deleteRecord']); + +export function calcShouldFetch( + store: Store, + request: ImmutableRequestInfo, + hasCachedValue: boolean, + identifier: StableDocumentIdentifier | null +): boolean { + const { cacheOptions } = request; + return ( + (request.op && MUTATION_OPS.has(request.op)) || + cacheOptions?.reload || + !hasCachedValue || + (store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier, store) : false) + ); +} + +export function calcShouldBackgroundFetch( + store: Store, + request: ImmutableRequestInfo, + willFetch: boolean, + identifier: StableDocumentIdentifier | null +): boolean { + const { cacheOptions } = request; + return ( + !willFetch && + (cacheOptions?.backgroundReload || + (store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier, store) : false)) + ); +} + +export function isMutation( + request: Partial +): request is ImmutableUpdateRequestOptions | ImmutableCreateRequestOptions | ImmutableDeleteRequestOptions { + return Boolean(request.op && MUTATION_OPS.has(request.op)); +} + +export function copyDocumentProperties(target: { links?: unknown; meta?: unknown; errors?: unknown }, source: object) { + if ('links' in source) { + target.links = source.links; + } + if ('meta' in source) { + target.meta = source.meta; + } + if ('errors' in source) { + target.errors = source.errors; + } +} + +export function isCacheAffecting(document: StructuredDataDocument): boolean { + if (!isMutation(document.request)) { + return true; + } + // a mutation combined with a 204 has no cache impact when no known records were involved + // a createRecord with a 201 with an empty response and no known records should similarly + // have no cache impact + + if (document.request.op === 'createRecord' && document.response?.status === 201) { + return document.content ? Object.keys(document.content).length > 0 : false; + } + + return document.response?.status !== 204; +} + +export function isAggregateError( + error: Error & { errors?: ApiError[] } +): error is AggregateError & { errors: ApiError[] } { + return error instanceof AggregateError || (error.name === 'AggregateError' && Array.isArray(error.errors)); +} + +export type RobustError = Error & { error: string | object; errors?: ApiError[]; content?: unknown }; + +// TODO @runspired, consider if we should deep freeze errors (potentially only in debug) vs cloning them +export function cloneError(error: RobustError) { + const isAggregate = isAggregateError(error); + + const cloned = ( + isAggregate ? new AggregateError(structuredClone(error.errors), error.message) : new Error(error.message) + ) as RobustError; + cloned.stack = error.stack!; + cloned.error = error.error; + + // copy over enumerable properties + Object.assign(cloned, error); + + return cloned; +} + +export function isErrorDocument( + document: ResourceDataDocument | ResourceErrorDocument +): document is ResourceErrorDocument { + return 'errors' in document; +} + +export function getPriority( + identifier: StableDocumentIdentifier | null, + deduped: Map, + priority: { blocking: boolean } +) { + if (identifier) { + const existing = deduped.get(identifier); + if (existing) { + return existing.priority; + } + } + return priority; +} diff --git a/packages/store/src/-private/caches/cache-utils.ts b/packages/store/src/-private/caches/cache-utils.ts index e03c1f8e535..4e31d7f83c4 100644 --- a/packages/store/src/-private/caches/cache-utils.ts +++ b/packages/store/src/-private/caches/cache-utils.ts @@ -1,17 +1,21 @@ -import { assert } from '@ember/debug'; +import { assert } from '@warp-drive/build-config/macros'; +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; /* * Returns the Cache instance associated with a given * Model or Identifier */ -export const CacheForIdentifierCache = new Map(); +export const CacheForIdentifierCache = getOrSetGlobal( + 'CacheForIdentifierCache', + new Map() +); -export function setCacheFor(identifier: StableRecordIdentifier | RecordInstance, cache: Cache): void { +export function setCacheFor(identifier: StableRecordIdentifier | OpaqueRecordInstance, cache: Cache): void { assert( `Illegal set of identifier`, !CacheForIdentifierCache.has(identifier) || CacheForIdentifierCache.get(identifier) === cache @@ -19,13 +23,13 @@ export function setCacheFor(identifier: StableRecordIdentifier | RecordInstance, CacheForIdentifierCache.set(identifier, cache); } -export function removeRecordDataFor(identifier: StableRecordIdentifier | RecordInstance): void { +export function removeRecordDataFor(identifier: StableRecordIdentifier | OpaqueRecordInstance): void { CacheForIdentifierCache.delete(identifier); } -export default function peekCache(instance: StableRecordIdentifier): Cache | null; -export default function peekCache(instance: RecordInstance): Cache; -export default function peekCache(instance: StableRecordIdentifier | RecordInstance): Cache | null { +export function peekCache(instance: StableRecordIdentifier): Cache | null; +export function peekCache(instance: OpaqueRecordInstance): Cache; +export function peekCache(instance: StableRecordIdentifier | OpaqueRecordInstance): Cache | null { if (CacheForIdentifierCache.has(instance as StableRecordIdentifier)) { return CacheForIdentifierCache.get(instance as StableRecordIdentifier) as Cache; } diff --git a/packages/store/src/-private/caches/identifier-cache.ts b/packages/store/src/-private/caches/identifier-cache.ts index d8e0a67f4fb..b6602336689 100644 --- a/packages/store/src/-private/caches/identifier-cache.ts +++ b/packages/store/src/-private/caches/identifier-cache.ts @@ -1,40 +1,49 @@ /** @module @ember-data/store */ -import { assert, warn } from '@ember/debug'; +import { warn } from '@ember/debug'; + +import { getGlobalConfig, macroCondition } from '@embroider/macros'; + +import { LOG_IDENTIFIERS } from '@warp-drive/build-config/debugging'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import { getOrSetGlobal, peekTransient, setTransient } from '@warp-drive/core-types/-private'; +import { + CACHE_OWNER, + DEBUG_CLIENT_ORIGINATED, + DEBUG_IDENTIFIER_BUCKET, + DEBUG_STALE_CACHE_OWNER, + type Identifier, + type IdentifierBucket, + type RecordIdentifier, + type StableDocumentIdentifier, + type StableIdentifier, + type StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; +import type { ExistingResourceObject, ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; -import { getOwnConfig, macroCondition } from '@embroider/macros'; - -import { LOG_IDENTIFIERS } from '@ember-data/debugging'; -import { DEBUG } from '@ember-data/env'; -import { ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { ExistingResourceObject, ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; import type { ForgetMethod, GenerationMethod, - Identifier, - IdentifierBucket, - RecordIdentifier, + KeyInfo, + KeyInfoMethod, ResetMethod, - ResourceData, - StableExistingRecordIdentifier, - StableRecordIdentifier, UpdateMethod, -} from '@ember-data/types/q/identifier'; -import type { ConfidentDict } from '@ember-data/types/q/utils'; - -import coerceId from '../utils/coerce-id'; -import { DEBUG_CLIENT_ORIGINATED, DEBUG_IDENTIFIER_BUCKET } from '../utils/identifier-debug-consts'; -import isNonEmptyString from '../utils/is-non-empty-string'; -import normalizeModelName from '../utils/normalize-model-name'; +} from '../../-types/q/identifier'; +import { coerceId } from '../utils/coerce-id'; +import { normalizeModelName } from '../utils/normalize-model-name'; import installPolyfill from '../utils/uuid-polyfill'; +import { hasId, hasLid, hasType } from './resource-utils'; -const IDENTIFIERS = new Set(); -const DOCUMENTS = new Set(); +type ResourceData = unknown; + +const IDENTIFIERS = getOrSetGlobal('IDENTIFIERS', new Set()); +const DOCUMENTS = getOrSetGlobal('DOCUMENTS', new Set()); export function isStableIdentifier(identifier: unknown): identifier is StableRecordIdentifier { - return IDENTIFIERS.has(identifier); + return (identifier as StableRecordIdentifier)[CACHE_OWNER] !== undefined || IDENTIFIERS.has(identifier); } export function isDocumentIdentifier(identifier: unknown): identifier is StableDocumentIdentifier { @@ -42,16 +51,16 @@ export function isDocumentIdentifier(identifier: unknown): identifier is StableD } const isFastBoot = typeof FastBoot !== 'undefined'; -const _crypto: Crypto = isFastBoot ? (FastBoot.require('crypto') as Crypto) : window.crypto; +const _crypto: Crypto = isFastBoot ? (FastBoot.require('crypto') as Crypto) : globalThis.crypto; -if (macroCondition(getOwnConfig<{ polyfillUUID: boolean }>().polyfillUUID)) { +if (macroCondition(getGlobalConfig<{ WarpDrive: { polyfillUUID: boolean } }>().WarpDrive.polyfillUUID)) { installPolyfill(); } function uuidv4(): string { assert( 'crypto.randomUUID needs to be avaliable. Some browsers incorrectly disallow it in insecure contexts. You maybe want to enable the polyfill: https://github.com/emberjs/data#randomuuid-polyfill', - _crypto.randomUUID + typeof _crypto.randomUUID === 'function' ); return _crypto.randomUUID(); } @@ -67,43 +76,90 @@ interface KeyOptions { lid: IdentifierMap; id: IdentifierMap; } +type TypeMap = { [key: string]: KeyOptions }; +// type IdentifierTypeLookup = { all: Set; id: Map }; +// type IdentifiersByType = Map; type IdentifierMap = Map; -type TypeMap = ConfidentDict; + +type StableCache = { + resources: IdentifierMap; + documents: Map; + resourcesByType: TypeMap; + polymorphicLidBackMap: Map; +}; + export type MergeMethod = ( targetIdentifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, - resourceData: ResourceIdentifierObject | ExistingResourceObject + resourceData: unknown ) => StableRecordIdentifier; -let configuredForgetMethod: ForgetMethod | null; -let configuredGenerationMethod: GenerationMethod | null; -let configuredResetMethod: ResetMethod | null; -let configuredUpdateMethod: UpdateMethod | null; - export function setIdentifierGenerationMethod(method: GenerationMethod | null): void { - configuredGenerationMethod = method; + setTransient('configuredGenerationMethod', method); } export function setIdentifierUpdateMethod(method: UpdateMethod | null): void { - configuredUpdateMethod = method; + setTransient('configuredUpdateMethod', method); } export function setIdentifierForgetMethod(method: ForgetMethod | null): void { - configuredForgetMethod = method; + setTransient('configuredForgetMethod', method); } export function setIdentifierResetMethod(method: ResetMethod | null): void { - configuredResetMethod = method; + setTransient('configuredResetMethod', method); } -type WithLid = { lid: string }; -type WithId = { id: string | null; type: string }; +export function setKeyInfoForResource(method: KeyInfoMethod | null): void { + setTransient('configuredKeyInfoMethod', method); +} function assertIsRequest(request: unknown): asserts request is ImmutableRequestInfo { return; } +// Map> +type TypeIdMap = Map>; +// TODO can we just delete this? +const NEW_IDENTIFIERS: TypeIdMap = new Map(); +// TODO @runspired maybe needs peekTransient ? +let IDENTIFIER_CACHE_ID = 0; + +function updateTypeIdMapping(typeMap: TypeIdMap, identifier: StableRecordIdentifier, id: string): void { + let idMap = typeMap.get(identifier.type); + if (!idMap) { + idMap = new Map(); + typeMap.set(identifier.type, idMap); + } + idMap.set(id, identifier.lid); +} + +function defaultUpdateMethod(identifier: StableRecordIdentifier, data: unknown, bucket: 'record'): void; +function defaultUpdateMethod(identifier: StableIdentifier, newData: unknown, bucket: never): void; +function defaultUpdateMethod( + identifier: StableIdentifier | StableRecordIdentifier, + data: unknown, + bucket: 'record' +): void { + if (bucket === 'record') { + assert(`Expected identifier to be a StableRecordIdentifier`, isStableIdentifier(identifier)); + if (!identifier.id && hasId(data)) { + updateTypeIdMapping(NEW_IDENTIFIERS, identifier, data.id); + } + } +} + +function defaultKeyInfoMethod(resource: unknown, known: StableRecordIdentifier | null): KeyInfo { + // TODO RFC something to make this configurable + const id = hasId(resource) ? coerceId(resource.id) : null; + const type = hasType(resource) ? normalizeModelName(resource.type) : known ? known.type : null; + + assert(`Expected keyInfoForResource to provide a type for the resource`, type); + + return { type, id }; +} + function defaultGenerationMethod(data: ImmutableRequestInfo, bucket: 'document'): string | null; function defaultGenerationMethod(data: ResourceData | { type: string }, bucket: 'record'): string; function defaultGenerationMethod( @@ -111,16 +167,19 @@ function defaultGenerationMethod( bucket: IdentifierBucket ): string | null { if (bucket === 'record') { - if (isNonEmptyString((data as WithLid).lid)) { - return (data as WithLid).lid; + if (hasLid(data)) { + return data.lid; } - if ((data as WithId).id !== undefined) { - let { type, id } = data as WithId; - // TODO: add test for id not a string - if (isNonEmptyString(coerceId(id))) { - return `@lid:${normalizeModelName(type)}-${id}`; - } + + assert(`Cannot generate an identifier for a resource without a type`, hasType(data)); + + if (hasId(data)) { + const type = normalizeModelName(data.type); + const lid = NEW_IDENTIFIERS.get(type)?.get(data.id); + + return lid || `@lid:${type}-${data.id}`; } + return uuidv4(); } else if (bucket === 'document') { assertIsRequest(data); @@ -132,14 +191,21 @@ function defaultGenerationMethod( } return null; } - assert(`Unknown bucket ${bucket}`, false); + assert(`Unknown bucket ${bucket as string}`, false); } -function defaultEmptyCallback(...args: any[]): any {} +function defaultEmptyCallback(...args: unknown[]): void {} +function defaultMergeMethod( + a: StableRecordIdentifier, + _b: StableRecordIdentifier, + _c: unknown +): StableRecordIdentifier { + return a; +} -let DEBUG_MAP; +let DEBUG_MAP: WeakMap; if (DEBUG) { - DEBUG_MAP = new WeakMap(); + DEBUG_MAP = getOrSetGlobal('DEBUG_MAP', new WeakMap()); } /** @@ -156,27 +222,33 @@ if (DEBUG) { @public */ export class IdentifierCache { - _cache = { - lids: new Map(), - types: Object.create(null) as TypeMap, - documents: new Map(), - }; + declare _cache: StableCache; declare _generate: GenerationMethod; declare _update: UpdateMethod; declare _forget: ForgetMethod; declare _reset: ResetMethod; declare _merge: MergeMethod; - declare _isDefaultConfig: boolean; + declare _keyInfoForResource: KeyInfoMethod; + declare _id: number; constructor() { // we cache the user configuredGenerationMethod at init because it must // be configured prior and is not allowed to be changed - this._generate = configuredGenerationMethod || (defaultGenerationMethod as GenerationMethod); - this._update = configuredUpdateMethod || defaultEmptyCallback; - this._forget = configuredForgetMethod || defaultEmptyCallback; - this._reset = configuredResetMethod || defaultEmptyCallback; - this._merge = defaultEmptyCallback; - this._isDefaultConfig = !configuredGenerationMethod; + this._generate = + peekTransient('configuredGenerationMethod') || (defaultGenerationMethod as GenerationMethod); + this._update = peekTransient('configuredUpdateMethod') || defaultUpdateMethod; + this._forget = peekTransient('configuredForgetMethod') || defaultEmptyCallback; + this._reset = peekTransient('configuredResetMethod') || defaultEmptyCallback; + this._merge = defaultMergeMethod; + this._keyInfoForResource = peekTransient('configuredKeyInfoMethod') || defaultKeyInfoMethod; + this._id = IDENTIFIER_CACHE_ID++; + + this._cache = { + resources: new Map(), + resourcesByType: Object.create(null) as TypeMap, + documents: new Map(), + polymorphicLidBackMap: new Map(), + }; } /** @@ -189,153 +261,85 @@ export class IdentifierCache { * @private */ __configureMerge(method: MergeMethod | null) { - this._merge = method || defaultEmptyCallback; + this._merge = method || defaultMergeMethod; + } + + upgradeIdentifier(resource: { type: string; id: string | null; lid?: string }): StableRecordIdentifier { + return this._getRecordIdentifier(resource, 2); } /** * @method _getRecordIdentifier * @private */ - _getRecordIdentifier(resource: ResourceIdentifierObject, shouldGenerate: true): StableRecordIdentifier; - _getRecordIdentifier(resource: ResourceIdentifierObject, shouldGenerate: false): StableRecordIdentifier | undefined; _getRecordIdentifier( - resource: ResourceIdentifierObject, - shouldGenerate: boolean = false - ): StableRecordIdentifier | undefined { + resource: { type: string; id: string | null; lid?: string }, + shouldGenerate: 2 + ): StableRecordIdentifier; + _getRecordIdentifier(resource: unknown, shouldGenerate: 1): StableRecordIdentifier; + _getRecordIdentifier(resource: unknown, shouldGenerate: 0): StableRecordIdentifier | undefined; + _getRecordIdentifier(resource: unknown, shouldGenerate: 0 | 1 | 2): StableRecordIdentifier | undefined { + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource); + } // short circuit if we're already the stable version if (isStableIdentifier(resource)) { if (DEBUG) { // TODO should we instead just treat this case as a new generation skipping the short circuit? - if (!this._cache.lids.has(resource.lid) || this._cache.lids.get(resource.lid) !== resource) { - throw new Error(`The supplied identifier ${resource} does not belong to this store instance`); + if (!this._cache.resources.has(resource.lid) || this._cache.resources.get(resource.lid) !== resource) { + throw new Error(`The supplied identifier ${JSON.stringify(resource)} does not belong to this store instance`); } } if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.log(`Identifiers: Peeked Identifier was already Stable ${String(resource)}`); - } - return resource; - } - - let lid = coerceId(resource.lid); - let identifier: StableRecordIdentifier | undefined = lid !== null ? this._cache.lids.get(lid) : undefined; - - if (identifier !== undefined) { - if (LOG_IDENTIFIERS) { + console.log(`Identifiers: cache HIT - Stable ${resource.lid}`); // eslint-disable-next-line no-console - console.log(`Identifiers: cache HIT ${identifier}`, resource); + console.groupEnd(); } - return identifier; + return resource; } + // the resource is unknown, ask the application to identify this data for us + const lid = this._generate(resource, 'record'); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource); + console.log(`Identifiers: ${lid ? 'no ' : ''}lid ${lid ? lid + ' ' : ''}determined for resource`, resource); } - if (shouldGenerate === false) { - if (!(resource as ExistingResourceObject).type || !(resource as ExistingResourceObject).id) { - return; + let identifier: StableRecordIdentifier | null = /*#__NOINLINE__*/ getIdentifierFromLid(this._cache, lid, resource); + if (identifier !== null) { + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.groupEnd(); } + return identifier; } - // `type` must always be present - assert('resource.type needs to be a string', 'type' in resource && isNonEmptyString(resource.type)); - - let type = resource.type && normalizeModelName(resource.type); - let id = coerceId(resource.id); - - let keyOptions = getTypeIndex(this._cache.types, type); - - // go straight for the stable RecordIdentifier key'd to `lid` - if (lid !== null) { - identifier = keyOptions.lid.get(lid); - } - - // we may have not seen this resource before - // but just in case we check our own secondary lookup (`id`) - if (identifier === undefined && id !== null) { - identifier = keyOptions.id.get(id); - } - - if (identifier === undefined) { - // we have definitely not seen this resource before - // so we allow the user configured `GenerationMethod` to tell us - let newLid = this._generate(resource, 'record'); + if (shouldGenerate === 0) { if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.log(`Identifiers: lid ${newLid} determined for resource`, resource); + console.groupEnd(); } + return; + } - // we do this _even_ when `lid` is present because secondary lookups - // may need to be populated, but we enforce not giving us something - // different than expected - if (lid !== null && newLid !== lid) { - throw new Error(`You should not change the of a RecordIdentifier`); - } else if (lid === null && !this._isDefaultConfig) { - // allow configuration to tell us that we have - // seen this `lid` before. E.g. a secondary lookup - // connects this resource to a previously seen - // resource. - identifier = keyOptions.lid.get(newLid); - } - - if (shouldGenerate === true) { - if (identifier === undefined) { - // if we still don't have an identifier, time to generate one - identifier = makeStableRecordIdentifier(id, type, newLid, 'record', false); - - // populate our unique table - if (DEBUG) { - // realistically if you hit this it means you changed `type` :/ - // TODO consider how to handle type change assertions more gracefully - if (this._cache.lids.has(identifier.lid)) { - throw new Error(`You should not change the of a RecordIdentifier`); - } - } - this._cache.lids.set(identifier.lid, identifier); - - // populate our primary lookup table - // TODO consider having the `lid` cache be - // one level up - keyOptions.lid.set(identifier.lid, identifier); - - if (LOG_IDENTIFIERS) { - if (shouldGenerate) { - // eslint-disable-next-line no-console - console.log(`Identifiers: generated ${String(identifier)} for`, resource); - if (resource[DEBUG_IDENTIFIER_BUCKET]) { - // eslint-disable-next-line no-console - console.trace( - `[WARNING] Identifiers: generated a new identifier from a previously used identifier. This is likely a bug.` - ); - } - } - } - } - - // populate our own secondary lookup table - // even for the "successful" secondary lookup - // by `_generate()`, since we missed the cache - // previously - // we use identifier.id instead of id here - // because they may not match and we prefer - // what we've set via resource data - if (identifier.id !== null) { - keyOptions.id.set(identifier.id, identifier); - - // TODO allow filling out of `id` here - // for the `username` non-client created - // case. - } - } + // if we still don't have an identifier, time to generate one + if (shouldGenerate === 2) { + (resource as StableRecordIdentifier).lid = lid; + (resource as StableRecordIdentifier)[CACHE_OWNER] = this._id; + identifier = /*#__NOINLINE__*/ makeStableRecordIdentifier(resource as StableRecordIdentifier, 'record', false); + } else { + // we lie a bit here as a memory optimization + const keyInfo = this._keyInfoForResource(resource, null) as StableRecordIdentifier; + keyInfo.lid = lid; + keyInfo[CACHE_OWNER] = this._id; + identifier = /*#__NOINLINE__*/ makeStableRecordIdentifier(keyInfo, 'record', false); } + addResourceToCache(this._cache, identifier); + if (LOG_IDENTIFIERS) { - if (!identifier && !shouldGenerate) { - // eslint-disable-next-line no-console - console.log(`Identifiers: cache MISS`, resource); - } // eslint-disable-next-line no-console console.groupEnd(); } @@ -350,11 +354,11 @@ export class IdentifierCache { * * @method peekRecordIdentifier * @param resource - * @returns {StableRecordIdentifier | undefined} + * @return {StableRecordIdentifier | undefined} * @private */ peekRecordIdentifier(resource: ResourceIdentifierObject | Identifier): StableRecordIdentifier | undefined { - return this._getRecordIdentifier(resource, false); + return this._getRecordIdentifier(resource, 0); } /** @@ -363,7 +367,7 @@ export class IdentifierCache { @method getOrCreateDocumentIdentifier @param request - @returns {StableDocumentIdentifier | null} + @return {StableDocumentIdentifier | null} @public */ getOrCreateDocumentIdentifier(request: ImmutableRequestInfo): StableDocumentIdentifier | null { @@ -403,15 +407,11 @@ export class IdentifierCache { @method getOrCreateRecordIdentifier @param resource - @returns {StableRecordIdentifier} + @return {StableRecordIdentifier} @public */ - getOrCreateRecordIdentifier(resource: ExistingResourceObject): StableExistingRecordIdentifier; - getOrCreateRecordIdentifier( - resource: ResourceIdentifierObject | Identifier | StableRecordIdentifier - ): StableRecordIdentifier; - getOrCreateRecordIdentifier(resource: ResourceData | Identifier): StableRecordIdentifier { - return this._getRecordIdentifier(resource, true); + getOrCreateRecordIdentifier(resource: unknown): StableRecordIdentifier { + return this._getRecordIdentifier(resource, 1); } /** @@ -424,27 +424,25 @@ export class IdentifierCache { @method createIdentifierForNewRecord @param data - @returns {StableRecordIdentifier} + @return {StableRecordIdentifier} @public */ createIdentifierForNewRecord(data: { type: string; id?: string | null }): StableRecordIdentifier { - let newLid = this._generate(data, 'record'); - let identifier = makeStableRecordIdentifier(data.id || null, data.type, newLid, 'record', true); - let keyOptions = getTypeIndex(this._cache.types, data.type); + const newLid = this._generate(data, 'record'); + const identifier = /*#__NOINLINE__*/ makeStableRecordIdentifier( + { id: data.id || null, type: data.type, lid: newLid, [CACHE_OWNER]: this._id }, + 'record', + true + ); // populate our unique table if (DEBUG) { - if (this._cache.lids.has(identifier.lid)) { + if (this._cache.resources.has(identifier.lid)) { throw new Error(`The lid generated for the new record is not unique as it matches an existing identifier`); } } - this._cache.lids.set(identifier.lid, identifier); - // populate the type+lid cache - keyOptions.lid.set(newLid, identifier); - if (data.id) { - keyOptions.id.set(data.id, identifier); - } + /*#__NOINLINE__*/ addResourceToCache(this._cache, identifier); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -473,40 +471,37 @@ export class IdentifierCache { @method updateRecordIdentifier @param identifierObject @param data - @returns {StableRecordIdentifier} + @return {StableRecordIdentifier} @public */ - updateRecordIdentifier(identifierObject: RecordIdentifier, data: ResourceData): StableRecordIdentifier { + updateRecordIdentifier(identifierObject: RecordIdentifier, data: unknown): StableRecordIdentifier { let identifier = this.getOrCreateRecordIdentifier(identifierObject); - let newId = - (data as ExistingResourceObject).id !== undefined ? coerceId((data as ExistingResourceObject).id) : null; - let existingIdentifier = detectMerge(this._cache.types, identifier, data, newId, this._cache.lids); + const keyInfo = this._keyInfoForResource(data, identifier); + let existingIdentifier = /*#__NOINLINE__*/ detectMerge(this._cache, keyInfo, identifier, data); + const hadLid = hasLid(data); if (!existingIdentifier) { // If the incoming type does not match the identifier type, we need to create an identifier for the incoming // data so we can merge the incoming data with the existing identifier, see #7325 and #7363 - if ( - (data as ExistingResourceObject).type && - identifier.type !== normalizeModelName((data as ExistingResourceObject).type) - ) { - let incomingDataResource = { ...data }; - // Need to strip the lid from the incomingData in order force a new identifier creation - delete incomingDataResource.lid; - existingIdentifier = this.getOrCreateRecordIdentifier(incomingDataResource); + if (identifier.type !== keyInfo.type) { + if (hadLid) { + // Strip the lid to ensure we force a new identifier creation + delete (data as { lid?: string }).lid; + } + existingIdentifier = this.getOrCreateRecordIdentifier(data); } } if (existingIdentifier) { - let keyOptions = getTypeIndex(this._cache.types, identifier.type); - let generatedIdentifier = identifier; - identifier = this._mergeRecordIdentifiers( - keyOptions, - generatedIdentifier, - existingIdentifier, - data, - newId as string - ); + const generatedIdentifier = identifier; + identifier = this._mergeRecordIdentifiers(keyInfo, generatedIdentifier, existingIdentifier, data); + + // make sure that the `lid` on the data we are processing matches the lid we kept + if (hadLid) { + data.lid = identifier.lid; + } + if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console console.log( @@ -516,24 +511,28 @@ export class IdentifierCache { } } - let id = identifier.id; - performRecordIdentifierUpdate(identifier, data, this._update); - newId = identifier.id; + const id = identifier.id; + /*#__NOINLINE__*/ performRecordIdentifierUpdate(identifier, keyInfo, data, this._update); + const newId = identifier.id; // add to our own secondary lookup table if (id !== newId && newId !== null) { if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console console.log( - `Identifiers: updated id for identifier ${identifier.lid} from '${id}' to '${newId}' for resource`, + `Identifiers: updated id for identifier ${identifier.lid} from '${String(id)}' to '${String( + newId + )}' for resource`, data ); } - let keyOptions = getTypeIndex(this._cache.types, identifier.type); - keyOptions.id.set(newId, identifier); + + const typeSet = this._cache.resourcesByType[identifier.type]; + assert(`Expected to find a typeSet for ${identifier.type}`, typeSet); + typeSet.id.set(newId, identifier); if (id !== null) { - keyOptions.id.delete(id); + typeSet.id.delete(id); } } else if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -548,28 +547,43 @@ export class IdentifierCache { * @private */ _mergeRecordIdentifiers( - keyOptions: KeyOptions, + keyInfo: KeyInfo, identifier: StableRecordIdentifier, existingIdentifier: StableRecordIdentifier, - data: ResourceIdentifierObject | ExistingResourceObject, - newId: string + data: unknown ): StableRecordIdentifier { + assert(`Expected keyInfo to contain an id`, hasId(keyInfo)); // delegate determining which identifier to keep to the configured MergeMethod - let kept = this._merge(identifier, existingIdentifier, data); - let abandoned = kept === identifier ? existingIdentifier : identifier; + const kept = this._merge(identifier, existingIdentifier, data); + const abandoned = kept === identifier ? existingIdentifier : identifier; + + // get any backreferences before forgetting this identifier, as it will be removed from the cache + // and we will no longer be able to find them + const abandonedBackReferences = this._cache.polymorphicLidBackMap.get(abandoned.lid); + // delete the backreferences for the abandoned identifier so that forgetRecordIdentifier + // does not try to remove them. + if (abandonedBackReferences) this._cache.polymorphicLidBackMap.delete(abandoned.lid); // cleanup the identifier we no longer need this.forgetRecordIdentifier(abandoned); - // ensure a secondary cache entry for this id for the identifier we do keep - keyOptions.id.set(newId, kept); - // ensure a secondary cache entry for this id for the abandoned identifier's type we do keep - let baseKeyOptions = getTypeIndex(this._cache.types, existingIdentifier.type); - baseKeyOptions.id.set(newId, kept); + // ensure a secondary cache entry for the original lid for the abandoned identifier + this._cache.resources.set(abandoned.lid, kept); - // make sure that the `lid` on the data we are processing matches the lid we kept - data.lid = kept.lid; + // backReferences let us know which other identifiers are pointing at this identifier + // so we can delete them later if we forget this identifier + const keptBackReferences = this._cache.polymorphicLidBackMap.get(kept.lid) ?? []; + keptBackReferences.push(abandoned.lid); + + // update the backreferences from the abandoned identifier to be for the kept identifier + if (abandonedBackReferences) { + abandonedBackReferences.forEach((lid) => { + keptBackReferences.push(lid); + this._cache.resources.set(lid, kept); + }); + } + this._cache.polymorphicLidBackMap.set(kept.lid, keptBackReferences); return kept; } @@ -586,15 +600,29 @@ export class IdentifierCache { @public */ forgetRecordIdentifier(identifierObject: RecordIdentifier): void { - let identifier = this.getOrCreateRecordIdentifier(identifierObject); - let keyOptions = getTypeIndex(this._cache.types, identifier.type); + const identifier = this.getOrCreateRecordIdentifier(identifierObject); + const typeSet = this._cache.resourcesByType[identifier.type]; + assert(`Expected to find a typeSet for ${identifier.type}`, typeSet); + if (identifier.id !== null) { - keyOptions.id.delete(identifier.id); + typeSet.id.delete(identifier.id); + } + this._cache.resources.delete(identifier.lid); + typeSet.lid.delete(identifier.lid); + + const backReferences = this._cache.polymorphicLidBackMap.get(identifier.lid); + if (backReferences) { + backReferences.forEach((lid) => { + this._cache.resources.delete(lid); + }); + this._cache.polymorphicLidBackMap.delete(identifier.lid); } - this._cache.lids.delete(identifier.lid); - keyOptions.lid.delete(identifier.lid); - IDENTIFIERS.delete(identifierObject); + if (DEBUG) { + 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 @@ -603,6 +631,7 @@ export class IdentifierCache { } destroy() { + NEW_IDENTIFIERS.clear(); this._cache.documents.forEach((identifier) => { DOCUMENTS.delete(identifier); }); @@ -610,38 +639,22 @@ export class IdentifierCache { } } -function getTypeIndex(typeMap: TypeMap, type: string): KeyOptions { - let typeIndex: KeyOptions = typeMap[type]; - - if (typeIndex === undefined) { - typeIndex = { - lid: new Map(), - id: new Map(), - }; - typeMap[type] = typeIndex; - } - - return typeIndex; -} - function makeStableRecordIdentifier( - id: string | null, - type: string, - lid: string, + recordIdentifier: { + type: string; + id: string | null; + lid: string; + [CACHE_OWNER]: number | undefined; + }, bucket: IdentifierBucket, - clientOriginated: boolean = false -): Readonly { - let recordIdentifier = { - lid, - id, - type, - }; + clientOriginated: boolean +): StableRecordIdentifier { IDENTIFIERS.add(recordIdentifier); if (DEBUG) { // we enforce immutability in dev // but preserve our ability to do controlled updates to the reference - let wrapper = { + let wrapper: StableRecordIdentifier = { get lid() { return recordIdentifier.lid; }, @@ -651,15 +664,33 @@ function makeStableRecordIdentifier( get type() { return recordIdentifier.type; }, - toString() { - let { type, id, lid } = recordIdentifier; - return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${type}:${id} (${lid})`; + get [CACHE_OWNER](): number | undefined { + return recordIdentifier[CACHE_OWNER]; }, - toJSON() { - let { type, id, lid } = recordIdentifier; - return { type, id, lid }; + set [CACHE_OWNER](value: number) { + recordIdentifier[CACHE_OWNER] = value; + }, + get [DEBUG_STALE_CACHE_OWNER](): number | undefined { + return (recordIdentifier as StableRecordIdentifier)[DEBUG_STALE_CACHE_OWNER]; + }, + set [DEBUG_STALE_CACHE_OWNER](value: number | undefined) { + (recordIdentifier as StableRecordIdentifier)[DEBUG_STALE_CACHE_OWNER] = value; }, }; + Object.defineProperty(wrapper, 'toString', { + enumerable: false, + value: () => { + const { type, id, lid } = recordIdentifier; + return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${String(type)}:${String(id)} (${lid})`; + }, + }); + Object.defineProperty(wrapper, 'toJSON', { + enumerable: false, + value: () => { + const { type, id, lid } = recordIdentifier; + return { type, id, lid }; + }, + }); wrapper[DEBUG_CLIENT_ORIGINATED] = clientOriginated; wrapper[DEBUG_IDENTIFIER_BUCKET] = bucket; IDENTIFIERS.add(wrapper); @@ -671,43 +702,42 @@ function makeStableRecordIdentifier( return recordIdentifier; } -function performRecordIdentifierUpdate(identifier: StableRecordIdentifier, data: ResourceData, updateFn: UpdateMethod) { +function performRecordIdentifierUpdate( + identifier: StableRecordIdentifier, + keyInfo: KeyInfo, + data: unknown, + updateFn: UpdateMethod +) { if (DEBUG) { - let { lid } = data; - let id = 'id' in data ? data.id : undefined; - let type = 'type' in data && data.type && normalizeModelName(data.type); + const { id, type } = keyInfo; // get the mutable instance behind our proxy wrapper - let wrapper = identifier; - identifier = DEBUG_MAP.get(wrapper); + const wrapper = identifier; + identifier = DEBUG_MAP.get(wrapper)!; - if (lid !== undefined) { - let newLid = coerceId(lid); - if (newLid !== identifier.lid) { + if (hasLid(data)) { + const lid = data.lid; + if (lid !== identifier.lid) { throw new Error( - `The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper}' to '${lid}'.` + `The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper.lid}' to '${lid}'.` ); } } - if (id !== undefined) { - let newId = coerceId(id); - - if (identifier.id !== null && identifier.id !== newId) { - // here we warn and ignore, as this may be a mistake, but we allow the user - // to have multiple cache-keys pointing at a single lid so we cannot error - warn( - `The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper}' to '${newId}'.`, - false, - { id: 'ember-data:multiple-ids-for-identifier' } - ); - } + if (id && identifier.id !== null && identifier.id !== id) { + // here we warn and ignore, as this may be a mistake, but we allow the user + // to have multiple cache-keys pointing at a single lid so we cannot error + warn( + `The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper.lid}' to '${id}'.`, + false, + { id: 'ember-data:multiple-ids-for-identifier' } + ); } // TODO consider just ignoring here to allow flexible polymorphic support if (type && type !== identifier.type) { throw new Error( - `The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper}' to '${type}'.` + `The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper.lid}' to '${type}'.` ); } @@ -726,32 +756,63 @@ function performRecordIdentifierUpdate(identifier: StableRecordIdentifier, data: } function detectMerge( - typesCache: ConfidentDict, + cache: StableCache, + keyInfo: KeyInfo, identifier: StableRecordIdentifier, - data: ResourceIdentifierObject | ExistingResourceObject, - newId: string | null, - lids: IdentifierMap + data: unknown ): StableRecordIdentifier | false { + const newId = keyInfo.id; const { id, type, lid } = identifier; + const typeSet = cache.resourcesByType[identifier.type]; + + // if the IDs are present but do not match + // then check if we have an existing identifier + // for the newer ID. if (id !== null && id !== newId && newId !== null) { - let keyOptions = getTypeIndex(typesCache, identifier.type); - let existingIdentifier = keyOptions.id.get(newId); + const existingIdentifier = typeSet && typeSet.id.get(newId); return existingIdentifier !== undefined ? existingIdentifier : false; } else { - let newType = (data as ExistingResourceObject).type && normalizeModelName((data as ExistingResourceObject).type); + const newType = keyInfo.type; // If the ids and type are the same but lid is not the same, we should trigger a merge of the identifiers - if (id !== null && id === newId && newType === type && data.lid && data.lid !== lid) { - let existingIdentifier = lids.get(data.lid); - return existingIdentifier !== undefined ? existingIdentifier : false; + // we trigger a merge of the identifiers + // though probably we should just throw an error here + if (id !== null && id === newId && newType === type && hasLid(data) && data.lid !== lid) { + return getIdentifierFromLid(cache, data.lid, data) || false; + // If the lids are the same, and ids are the same, but types are different we should trigger a merge of the identifiers - } else if (id !== null && id === newId && newType && newType !== type && data.lid && data.lid === lid) { - let keyOptions = getTypeIndex(typesCache, newType); - let existingIdentifier = keyOptions.id.get(id); + } else if (id !== null && id === newId && newType && newType !== type && hasLid(data) && data.lid === lid) { + const newTypeSet = cache.resourcesByType[newType]; + const existingIdentifier = newTypeSet && newTypeSet.id.get(newId); + return existingIdentifier !== undefined ? existingIdentifier : false; } } return false; } + +function getIdentifierFromLid(cache: StableCache, lid: string, resource: unknown): StableRecordIdentifier | null { + const identifier = cache.resources.get(lid); + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.log(`Identifiers: cache ${identifier ? 'HIT' : 'MISS'} - Non-Stable ${lid}`, resource); + } + return identifier || null; +} + +function addResourceToCache(cache: StableCache, identifier: StableRecordIdentifier): void { + cache.resources.set(identifier.lid, identifier); + let typeSet = cache.resourcesByType[identifier.type]; + + if (!typeSet) { + typeSet = { lid: new Map(), id: new Map() }; + cache.resourcesByType[identifier.type] = typeSet; + } + + typeSet.lid.set(identifier.lid, identifier); + if (identifier.id) { + typeSet.id.set(identifier.id, identifier); + } +} diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 22e3b473841..1fa2a0bd4f6 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -1,61 +1,45 @@ -import { assert, deprecate, warn } from '@ember/debug'; - -import { importSync } from '@embroider/macros'; - -import { LOG_INSTANCE_CACHE } from '@ember-data/debugging'; -import { - DEPRECATE_INSTANTIATE_RECORD_ARGS, - DEPRECATE_V1_RECORD_DATA, - DEPRECATE_V1CACHE_STORE_APIS, -} from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import type { Graph } from '@ember-data/graph/-private/graph/graph'; -import type { peekGraph } from '@ember-data/graph/-private/graph/index'; -import { HAS_GRAPH_PACKAGE, HAS_JSON_API_PACKAGE } from '@ember-data/packages'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper as StoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; +import { warn } from '@ember/debug'; + +import { LOG_INSTANCE_CACHE } from '@warp-drive/build-config/debugging'; +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 { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypedRecordInstance, TypeFromInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; +import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; import type { ExistingResourceIdentifierObject, ExistingResourceObject, - NewResourceIdentifierObject, -} from '@ember-data/types/q/ember-data-json-api'; -import type { - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import type { JsonApiRelationship, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { Dict } from '@ember-data/types/q/utils'; + InnerRelationshipDocument, +} from '@warp-drive/core-types/spec/json-api-raw'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import RecordReference from '../legacy-model-support/record-reference'; -import { NonSingletonCacheManager } from '../managers/cache-manager'; -import { CacheStoreWrapper } from '../managers/cache-store-wrapper'; -import type { CreateRecordProperties } from '../store-service'; -import type Store from '../store-service'; -import coerceId, { ensureStringId } from '../utils/coerce-id'; -import constructResource from '../utils/construct-resource'; -import normalizeModelName from '../utils/normalize-model-name'; +import { CacheCapabilitiesManager } from '../managers/cache-capabilities-manager'; +import type { CacheManager } from '../managers/cache-manager'; +import type { CreateRecordProperties, Store } from '../store-service'; +import { ensureStringId } from '../utils/coerce-id'; import { CacheForIdentifierCache, removeRecordDataFor, setCacheFor } from './cache-utils'; -let _peekGraph: peekGraph; -if (HAS_GRAPH_PACKAGE) { - let __peekGraph: peekGraph; - _peekGraph = (wrapper: Store | StoreWrapper): Graph | undefined => { - let a = (importSync('@ember-data/graph/-private') as { peekGraph: peekGraph }).peekGraph; - __peekGraph = __peekGraph || a; - return __peekGraph(wrapper); - }; +type Destroyable = { + isDestroyed: boolean; + isDestroying: boolean; + destroy(): void; +}; + +function isDestroyable(record: OpaqueRecordInstance): record is Destroyable { + return Boolean(record && typeof record === 'object' && typeof (record as Destroyable).destroy === 'function'); } /** @module @ember-data/store */ -const RecordCache = new Map(); +const RecordCache = getOrSetGlobal('RecordCache', new Map()); -export function peekRecordIdentifier(record: RecordInstance): StableRecordIdentifier | undefined { +export function peekRecordIdentifier(record: OpaqueRecordInstance): StableRecordIdentifier | undefined { return RecordCache.get(record); } @@ -76,14 +60,18 @@ export function peekRecordIdentifier(record: RecordInstance): StableRecordIdenti @static @for @ember-data/store @param {Object} record a record instance previously obstained from the store. - @returns {StableRecordIdentifier} + @return {StableRecordIdentifier} */ -export function recordIdentifierFor(record: RecordInstance): StableRecordIdentifier { +export function recordIdentifierFor( + record: T +): StableRecordIdentifier>; +export function recordIdentifierFor(record: OpaqueRecordInstance): StableRecordIdentifier; +export function recordIdentifierFor(record: T): StableRecordIdentifier> { assert(`${String(record)} is not a record instantiated by @ember-data/store`, RecordCache.has(record)); - return RecordCache.get(record)!; + return RecordCache.get(record)! as StableRecordIdentifier>; } -export function setRecordIdentifier(record: RecordInstance, identifier: StableRecordIdentifier): void { +export function setRecordIdentifier(record: OpaqueRecordInstance, identifier: StableRecordIdentifier): void { if (DEBUG) { if (RecordCache.has(record) && RecordCache.get(record) !== identifier) { throw new Error(`${String(record)} was already assigned an identifier`); @@ -101,9 +89,9 @@ export function setRecordIdentifier(record: RecordInstance, identifier: StableRe RecordCache.set(record, identifier); } -export const StoreMap = new Map(); +export const StoreMap = getOrSetGlobal('StoreMap', new Map()); -export function storeFor(record: RecordInstance): Store | undefined { +export function storeFor(record: OpaqueRecordInstance): Store | undefined { const store = StoreMap.get(record); assert( @@ -114,53 +102,41 @@ export function storeFor(record: RecordInstance): Store | undefined { } type Caches = { - record: Map; - resourceCache: Map; + record: Map; reference: WeakMap; }; export class InstanceCache { declare store: Store; declare cache: Cache; - declare _storeWrapper: CacheStoreWrapper; - declare __cacheFor: (resource: RecordIdentifier) => Cache; + declare _storeWrapper: CacheCapabilitiesManager; - declare __cacheManager: NonSingletonCacheManager; + declare __cacheManager: CacheManager; __instances: Caches = { - record: new Map(), - resourceCache: new Map(), + record: new Map(), reference: new WeakMap(), }; constructor(store: Store) { this.store = store; - this._storeWrapper = new CacheStoreWrapper(this.store); - - if (DEPRECATE_V1_RECORD_DATA) { - this.__cacheFor = (resource: RecordIdentifier) => { - // TODO enforce strict - const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); - return this.getResourceCache(identifier); - }; - } + this._storeWrapper = new CacheCapabilitiesManager(this.store); store.identifierCache.__configureMerge( - (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData) => { + (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData: unknown) => { let keptIdentifier = identifier; if (identifier.id !== matchedIdentifier.id) { + // @ts-expect-error TODO this needs to be fixed keptIdentifier = 'id' in resourceData && identifier.id === resourceData.id ? identifier : matchedIdentifier; } else if (identifier.type !== matchedIdentifier.type) { - keptIdentifier = + keptIdentifier = // @ts-expect-error TODO this needs to be fixed 'type' in resourceData && identifier.type === resourceData.type ? identifier : matchedIdentifier; } - let staleIdentifier = identifier === keptIdentifier ? matchedIdentifier : identifier; + const staleIdentifier = identifier === keptIdentifier ? matchedIdentifier : identifier; // check for duplicate entities - let keptHasRecord = this.__instances.record.has(keptIdentifier); - let staleHasRecord = this.__instances.record.has(staleIdentifier); - let keptResourceCache = this.__instances.resourceCache.get(keptIdentifier) || null; - let staleResourceCache = this.__instances.resourceCache.get(staleIdentifier) || null; + const keptHasRecord = this.__instances.record.has(keptIdentifier); + const staleHasRecord = this.__instances.record.has(staleIdentifier); // we cannot merge entities when both have records // (this may not be strictly true, we could probably swap the cache data the record points at) @@ -169,6 +145,7 @@ export class InstanceCache { // we can probably just "swap" what data source the abandoned // record points at so long as // it itself is not retained by the store in any way. + // @ts-expect-error TODO this needs to be fixed if ('id' in resourceData) { throw new Error( `Failed to update the 'id' for the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${ @@ -188,39 +165,16 @@ export class InstanceCache { ); } - let resourceCache = keptResourceCache || staleResourceCache; - - if (resourceCache) { - resourceCache.patch({ - op: 'mergeIdentifiers', - record: staleIdentifier, - value: keptIdentifier, - }); - } else if (!DEPRECATE_V1_RECORD_DATA) { - this.store.cache.patch({ - op: 'mergeIdentifiers', - record: staleIdentifier, - value: keptIdentifier, - }); - } else if (HAS_JSON_API_PACKAGE) { - this.store.cache.patch({ - op: 'mergeIdentifiers', - record: staleIdentifier, - value: keptIdentifier, - }); - } - - if (staleResourceCache === null) { - return keptIdentifier; - } + this.store.cache.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); /* TODO @runspired consider adding this to make polymorphism even nicer - if (HAS_GRAPH_PACKAGE) { - if (identifier.type !== matchedIdentifier.type) { - const graphFor = importSync('@ember-data/graph/-private').graphFor; - graphFor(this).registerPolymorphicType(identifier.type, matchedIdentifier.type); - } + if (identifier.type !== matchedIdentifier.type) { + this.store._graph?.registerPolymorphicType(identifier.type, matchedIdentifier.type); } */ @@ -229,19 +183,11 @@ export class InstanceCache { } ); } - peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined; - peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'resourceCache' }): Cache | undefined; - peek({ - identifier, - bucket, - }: { - identifier: StableRecordIdentifier; - bucket: 'record' | 'resourceCache'; - }): Cache | RecordInstance | undefined { - return this.__instances[bucket]?.get(identifier); + peek(identifier: StableRecordIdentifier): Cache | OpaqueRecordInstance | undefined { + return this.__instances.record.get(identifier); } - getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance { + getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): OpaqueRecordInstance { let record = this.__instances.record.get(identifier); if (!record) { @@ -249,44 +195,10 @@ export class InstanceCache { `Cannot create a new record instance while the store is being destroyed`, !this.store.isDestroying && !this.store.isDestroyed ); - const cache = this.getResourceCache(identifier); - - if (DEPRECATE_INSTANTIATE_RECORD_ARGS) { - if (this.store.instantiateRecord.length > 2) { - deprecate( - `Expected store.instantiateRecord to have an arity of 2. recordDataFor and notificationManager args have been deprecated.`, - false, - { - for: '@ember-data/store', - id: 'ember-data:deprecate-instantiate-record-args', - since: { available: '4.12', enabled: '4.12' }, - until: '5.0', - } - ); - } - assert( - `Cannot create a record for ${identifier.type + ':' + String(identifier.id)} (${ - identifier.lid - }) as no resource data exists`, - // @ts-expect-error managedVersion is private and debug only - Boolean(cache.managedVersion === '1' || cache.peek(identifier)) - ); - record = this.store.instantiateRecord( - identifier, - properties || {}, - // @ts-expect-error - this.__cacheFor, - this.store.notifications - ); - } else { - assert( - `Cannot create a record for ${identifier.type + ':' + String(identifier.id)} (${ - identifier.lid - }) as no resource data exists`, - cache.peek(identifier) - ); - record = this.store.instantiateRecord(identifier, properties || {}); - } + const cache = this.store.cache; + setCacheFor(identifier, cache); + + record = this.store.instantiateRecord(identifier, properties || {}); setRecordIdentifier(record, identifier); setCacheFor(record, cache); @@ -302,71 +214,8 @@ export class InstanceCache { return record; } - getResourceCache(identifier: StableRecordIdentifier): Cache { - if (!DEPRECATE_V1_RECORD_DATA) { - const cache = this.store.cache; - setCacheFor(identifier, cache); - - this.__instances.resourceCache.set(identifier, cache); - return cache; - } - - let cache = this.__instances.resourceCache.get(identifier); - - if (cache) { - return cache; - } - - if (this.store.createRecordDataFor) { - deprecate( - `Store.createRecordDataFor(, , , ) has been deprecated in favor of Store.createCache()`, - false, - { - id: 'ember-data:deprecate-v1-cache', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.12', available: '4.12' }, - } - ); - - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (this.store.createRecordDataFor.length > 2) { - let cacheInstance = this.store.createRecordDataFor( - identifier.type, - identifier.id, - // @ts-expect-error - identifier.lid, - this._storeWrapper - ); - cache = new NonSingletonCacheManager(this.store, cacheInstance, identifier); - } - } - - if (!cache) { - let cacheInstance = this.store.createRecordDataFor(identifier, this._storeWrapper); - - cache = - cacheInstance.version === '2' - ? cacheInstance - : new NonSingletonCacheManager(this.store, cacheInstance, identifier); - } - } else { - cache = this.store.cache; - } - - setCacheFor(identifier, cache); - - this.__instances.resourceCache.set(identifier, cache); - if (LOG_INSTANCE_CACHE) { - // eslint-disable-next-line no-console - console.log(`InstanceCache: created Cache for ${String(identifier)}`); - } - - return cache; - } - getReference(identifier: StableRecordIdentifier) { - let cache = this.__instances.reference; + const cache = this.__instances.reference; let reference = cache.get(identifier); if (!reference) { @@ -376,8 +225,8 @@ export class InstanceCache { return reference; } - recordIsLoaded(identifier: StableRecordIdentifier, filterDeleted: boolean = false) { - const cache = DEPRECATE_V1_RECORD_DATA ? this.__instances.resourceCache.get(identifier) || this.cache : this.cache; + recordIsLoaded(identifier: StableRecordIdentifier, filterDeleted = false) { + const cache = this.cache; if (!cache) { return false; } @@ -402,18 +251,12 @@ export class InstanceCache { const record = this.__instances.record.get(identifier); assert( 'Cannot destroy record while it is still materialized', - !record || record.isDestroyed || record.isDestroying + !isDestroyable(record) || record.isDestroyed || record.isDestroying ); - if (HAS_GRAPH_PACKAGE) { - let graph = _peekGraph(this.store); - if (graph) { - graph.remove(identifier); - } - } + this.store._graph?.remove(identifier); this.store.identifierCache.forgetRecordIdentifier(identifier); - this.__instances.resourceCache.delete(identifier); removeRecordDataFor(identifier); this.store._requestCache._clearEntries(identifier); if (LOG_INSTANCE_CACHE) { @@ -441,7 +284,7 @@ export class InstanceCache { // TODO is this join still necessary? this.store._join(() => { const record = this.__instances.record.get(identifier); - const cache = DEPRECATE_V1_RECORD_DATA ? this.__instances.resourceCache.get(identifier) : this.cache; + const cache = this.cache; if (record) { this.store.teardownRecord(record); @@ -458,7 +301,6 @@ export class InstanceCache { if (cache) { cache.unloadRecord(identifier); - this.__instances.resourceCache.delete(identifier); removeRecordDataFor(identifier); if (LOG_INSTANCE_CACHE) { // eslint-disable-next-line no-console @@ -483,13 +325,12 @@ export class InstanceCache { if (type === undefined) { // it would be cool if we could just de-ref cache here // but probably would require WeakRef models to do so. - cache.lids.forEach((identifier) => { + cache.resources.forEach((identifier) => { this.unloadRecord(identifier); }); } else { - const typeCache = cache.types; - let identifiers = typeCache[type]?.lid; - // const rds = this.__instances.resourceCache; + const typeCache = cache.resourcesByType; + const identifiers = typeCache[type]?.lid; if (identifiers) { identifiers.forEach((identifier) => { // if (rds.has(identifier)) { @@ -504,7 +345,7 @@ export class InstanceCache { // TODO this should move into something coordinating operations setRecordId(identifier: StableRecordIdentifier, id: string) { const { type, lid } = identifier; - let oldId = identifier.id; + const oldId = identifier.id; // ID absolutely can't be missing if the oldID is empty (missing Id in response for a new record) assert( @@ -534,7 +375,7 @@ export class InstanceCache { console.log(`InstanceCache: updating id to '${id}' for record ${String(identifier)}`); } - let existingIdentifier = this.store.identifierCache.peekRecordIdentifier({ type, id }); + const existingIdentifier = this.store.identifierCache.peekRecordIdentifier({ type, id }); assert( `'${type}' was saved to the server, but the response returned the new id '${id}', which has already been used with another record.'`, !existingIdentifier || existingIdentifier === identifier @@ -549,46 +390,6 @@ export class InstanceCache { // TODO handle consequences of identifier merge for notifications this.store.notifications.notify(identifier, 'identity'); } - - // TODO ths should be wrapped in a deprecation flag since cache.put - // handles this the rest of the time - loadData(data: ExistingResourceObject): StableExistingRecordIdentifier { - let modelName = data.type; - assert( - `You must include an 'id' for ${modelName} in an object passed to 'push'`, - data.id !== null && data.id !== undefined && data.id !== '' - ); - assert( - `You tried to push data with a type '${modelName}' but no model could be found with that name.`, - this.store.getSchemaDefinitionService().doesTypeExist(modelName) - ); - - const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid)); - let identifier = this.store.identifierCache.peekRecordIdentifier(resource); - let isUpdate = false; - - // store.push will be from empty - // findRecord will be from root.loading - // this cannot be loading state if we do not already have an identifier - // all else will be updates - if (identifier) { - const isLoading = _isLoading(this, identifier) || !this.recordIsLoaded(identifier); - isUpdate = !_isEmpty(this, identifier) && !isLoading; - - // exclude store.push (root.empty) case - if (isUpdate || isLoading) { - identifier = this.store.identifierCache.updateRecordIdentifier(identifier, data); - } - } else { - identifier = this.store.identifierCache.getOrCreateRecordIdentifier(data); - } - - const cache = this.getResourceCache(identifier); - const hasRecord = this.__instances.record.has(identifier); - cache.upsert(identifier, data, hasRecord); - - return identifier as StableExistingRecordIdentifier; - } } function _resourceIsFullDeleted(identifier: StableRecordIdentifier, cache: Cache): boolean { @@ -596,9 +397,7 @@ function _resourceIsFullDeleted(identifier: StableRecordIdentifier, cache: Cache } export function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: StableRecordIdentifier): boolean { - const cache = DEPRECATE_V1_RECORD_DATA - ? instanceCache.__instances.resourceCache.get(identifier) - : instanceCache.cache; + const cache = instanceCache.cache; return !cache || _resourceIsFullDeleted(identifier, cache); } @@ -613,24 +412,21 @@ export function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Preloaded data can be attributes and relationships passed in either as IDs or as actual models. */ -type PreloadRelationshipValue = RecordInstance | string; -export function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Dict) { - let jsonPayload: JsonApiResource = {}; +type PreloadRelationshipValue = OpaqueRecordInstance | string; +export function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Record) { + const jsonPayload: Partial = {}; //TODO(Igor) consider the polymorphic case - const schemas = store.getSchemaDefinitionService(); - const relationships = schemas.relationshipsDefinitionFor(identifier); + const schemas = store.schema; + const fields = schemas.fields(identifier); Object.keys(preload).forEach((key) => { - let preloadValue = preload[key]; + const preloadValue = preload[key]; - let relationshipMeta = relationships[key]; - if (relationshipMeta) { + const field = fields.get(key); + if (field && (field.kind === 'hasMany' || field.kind === 'belongsTo')) { if (!jsonPayload.relationships) { jsonPayload.relationships = {}; } - jsonPayload.relationships[key] = preloadRelationship( - relationshipMeta, - preloadValue as PreloadRelationshipValue | null | Array - ); + jsonPayload.relationships[key] = preloadRelationship(field, preloadValue); } else { if (!jsonPayload.attributes) { jsonPayload.attributes = {}; @@ -638,15 +434,15 @@ export function preloadData(store: Store, identifier: StableRecordIdentifier, pr jsonPayload.attributes[key] = preloadValue; } }); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; - const hasRecord = Boolean(store._instanceCache.peek({ identifier, bucket: 'record' })); + const cache = store.cache; + const hasRecord = Boolean(store._instanceCache.peek(identifier)); cache.upsert(identifier, jsonPayload, hasRecord); } function preloadRelationship( schema: RelationshipSchema, preloadValue: PreloadRelationshipValue | null | Array -): JsonApiRelationship { +): InnerRelationshipDocument { const relatedType = schema.type; if (schema.kind === 'hasMany') { @@ -663,41 +459,15 @@ function preloadRelationship( findRecord('user', '1', { preload: { friends: [record] }}); */ function _convertPreloadRelationshipToJSON( - value: RecordInstance | string, + value: OpaqueRecordInstance | string, type: string -): ExistingResourceIdentifierObject | NewResourceIdentifierObject { +): ExistingResourceIdentifierObject { if (typeof value === 'string' || typeof value === 'number') { - return { type, id: value }; + return { type, id: ensureStringId(value) }; } // TODO if not a record instance assert it's an identifier // and allow identifiers to be used - return recordIdentifierFor(value); -} - -function _isEmpty(instanceCache: InstanceCache, identifier: StableRecordIdentifier): boolean { - const cache = DEPRECATE_V1_RECORD_DATA - ? instanceCache.__instances.resourceCache.get(identifier) - : instanceCache.cache; - if (!cache) { - return true; - } - const isNew = cache.isNew(identifier); - const isDeleted = cache.isDeleted(identifier); - const isEmpty = cache.isEmpty(identifier); - - return (!isNew || isDeleted) && isEmpty; -} - -function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): boolean { - const req = cache.store.getRequestStateService(); - // const fulfilled = req.getLastRequestForRecord(identifier); - const isLoaded = cache.recordIsLoaded(identifier); - - return ( - !isLoaded && - // fulfilled === null && - req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query') - ); + return recordIdentifierFor(value) as ExistingResourceIdentifierObject; } export function _clearCaches() { diff --git a/packages/store/src/-private/caches/resource-utils.ts b/packages/store/src/-private/caches/resource-utils.ts new file mode 100644 index 00000000000..181b1d5f19d --- /dev/null +++ b/packages/store/src/-private/caches/resource-utils.ts @@ -0,0 +1,23 @@ +function isResource(resource: unknown): resource is Record { + return Boolean(resource && typeof resource === 'object'); +} + +function hasProp(resource: unknown, prop: T): resource is K { + return Boolean( + isResource(resource) && prop in resource && typeof resource[prop] === 'string' && resource[prop].length + ); +} + +export function hasLid(resource: unknown): resource is { lid: string } { + return hasProp(resource, 'lid'); +} + +export function hasId(resource: unknown): resource is { id: string } { + return ( + hasProp(resource, 'id') || Boolean(isResource(resource) && 'id' in resource && typeof resource.id === 'number') + ); +} + +export function hasType(resource: unknown): resource is { type: string } { + return hasProp(resource, 'type'); +} diff --git a/packages/store/src/-private/document.ts b/packages/store/src/-private/document.ts index fafc12c7bfc..ea20311ae34 100644 --- a/packages/store/src/-private/document.ts +++ b/packages/store/src/-private/document.ts @@ -1,23 +1,88 @@ -import { assert } from '@ember/debug'; -import { tracked } from '@glimmer/tracking'; +/** + * @module @ember-data/store + */ +import { defineSignal } from '@ember-data/tracking/-private'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; +import type { RequestInfo } from '@warp-drive/core-types/request'; +import type { Link, Meta, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; -import { RequestInfo } from '@ember-data/request/-private/types'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import { Link, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; - -import type Store from './store-service'; +import type { Store } from './store-service'; function urlFromLink(link: Link): string { if (typeof link === 'string') return link; return link.href; } +/** + * A Document is a class that wraps the response content from a request to the API + * returned by `Cache.put` or `Cache.peek`, converting resource-identifiers into + * record instances. + * + * It is not directly instantiated by the user, and its properties should not + * be directly modified. Whether individual properties are mutable or not is + * determined by the record instance itself. + * + * @public + * @class Document + */ export class Document { - @tracked links?: PaginationLinks; - @tracked data?: T; - @tracked errors?: object; - @tracked meta?: object; + /** + * The links object for this document, if any + * + * e.g. + * + * ``` + * { + * self: '/articles?page[number]=3', + * } + * ``` + * + * @property links + * @type {object|undefined} - a links object + * @public + */ + declare links?: PaginationLinks; + /** + * The primary data for this document, if any. + * + * If this document has no primary data (e.g. because it is an error document) + * this property will be `undefined`. + * + * For collections this will be an array of record instances, + * for single resource requests it will be a single record instance or null. + * + * @property data + * @public + * @type {object|Array|null|undefined} - a data object + */ + declare data?: T; + + /** + * The errors returned by the API for this request, if any + * + * @property errors + * @public + * @type {object|undefined} - an errors object + */ + declare errors?: object[]; + /** + * The meta object for this document, if any + * + * @property meta + * @public + * @type {object|undefined} - a meta object + */ + declare meta?: Meta; + + /** + * The identifier associated with this document, if any + * + * @property identifier + * @public + * @type {StableDocumentIdentifier|null} + */ declare identifier: StableDocumentIdentifier | null; #store: Store; @@ -26,40 +91,108 @@ export class Document { this.identifier = identifier; } - async #request(link: keyof PaginationLinks, options: object = {}): Promise | null> { + async #request( + link: keyof PaginationLinks, + options: Partial>> + ): Promise | null> { const href = this.links?.[link]; if (!href) { return null; } - const response = await this.#store.request>(Object.assign(options, { url: urlFromLink(href) })); + options.method = options.method || 'GET'; + Object.assign(options, { url: urlFromLink(href) }); + const response = await this.#store.request>(options); return response.content; } - fetch(options: Partial = {}): Promise> { - assert(`No self link`, this.links?.self); + /** + * Fetches the related link for this document, returning a promise that resolves + * with the document when the request completes. If no related link is present, + * will fallback to the self link if present + * + * @method fetch + * @public + * @param {object} options + * @return Promise + */ + fetch(options: Partial>> = {}): Promise> { + assert(`No self or related link`, this.links?.related || this.links?.self); options.cacheOptions = options.cacheOptions || {}; options.cacheOptions.key = this.identifier?.lid; - return this.#request('self', options) as Promise>; + return this.#request(this.links.related ? 'related' : 'self', options) as Promise>; } - next(options?: object): Promise | null> { + /** + * Fetches the next link for this document, returning a promise that resolves + * with the new document when the request completes, or null if there is no + * next link. + * + * @method next + * @public + * @param {object} options + * @return Promise + */ + next(options: Partial>> = {}): Promise | null> { return this.#request('next', options); } - prev(options?: object): Promise | null> { + /** + * Fetches the prev link for this document, returning a promise that resolves + * with the new document when the request completes, or null if there is no + * prev link. + * + * @method prev + * @public + * @param {object} options + * @return Promise + */ + prev(options: Partial>> = {}): Promise | null> { return this.#request('prev', options); } - first(options?: object): Promise | null> { + /** + * Fetches the first link for this document, returning a promise that resolves + * with the new document when the request completes, or null if there is no + * first link. + * + * @method first + * @public + * @param {object} options + * @return Promise + */ + first(options: Partial>> = {}): Promise | null> { return this.#request('first', options); } - last(options?: object): Promise | null> { + /** + * Fetches the last link for this document, returning a promise that resolves + * with the new document when the request completes, or null if there is no + * last link. + * + * @method last + * @public + * @param {object} options + * @return Promise + */ + last(options: Partial>> = {}): Promise | null> { return this.#request('last', options); } + /** + * Implemented for `JSON.stringify` support. + * + * Returns the JSON representation of the document wrapper. + * + * This is a shallow serialization, it does not deeply serialize + * the document's contents, leaving that to the individual record + * instances to determine how to do, if at all. + * + * @method toJSON + * @public + * @return + */ toJSON(): object { const data: Partial> = {}; data.identifier = this.identifier; @@ -78,3 +211,8 @@ export class Document { return data; } } + +defineSignal(Document.prototype, 'data'); +defineSignal(Document.prototype, 'links'); +defineSignal(Document.prototype, 'errors'); +defineSignal(Document.prototype, 'meta'); diff --git a/packages/store/src/-private/index.ts b/packages/store/src/-private/index.ts deleted file mode 100644 index 1f886577a52..00000000000 --- a/packages/store/src/-private/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - @module @ember-data/store -*/ - -import { assert, deprecate } from '@ember/debug'; - -import { DEPRECATE_HELPERS } from '@ember-data/deprecations'; - -import _normalize from './utils/normalize-model-name'; - -export { default as Store, storeFor } from './store-service'; - -export { recordIdentifierFor } from './caches/instance-cache'; - -export { CacheHandler } from './cache-handler'; - -export { - setIdentifierGenerationMethod, - setIdentifierUpdateMethod, - setIdentifierForgetMethod, - setIdentifierResetMethod, - isStableIdentifier, -} from './caches/identifier-cache'; - -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`); -} - -export { default as constructResource } from './utils/construct-resource'; - -// TODO this should be a deprecated helper but we have so much usage of it -// to also eliminate -export { default as coerceId, ensureStringId } from './utils/coerce-id'; - -export { - default as RecordArray, - default as IdentifierArray, - Collection as AdapterPopulatedRecordArray, - notifyArray, - SOURCE, - MUTATE, - IDENTIFIER_ARRAY_TAG, -} from './record-arrays/identifier-array'; -export { default as RecordArrayManager, fastPush } from './managers/record-array-manager'; - -// leaked for private use / test use, should investigate removing -export { _clearCaches } from './caches/instance-cache'; -export { default as peekCache, removeRecordDataFor } from './caches/cache-utils'; diff --git a/packages/store/src/-private/legacy-model-support/record-reference.ts b/packages/store/src/-private/legacy-model-support/record-reference.ts index 0903eb7ca94..fb7d8ea8ea1 100644 --- a/packages/store/src/-private/legacy-model-support/record-reference.ts +++ b/packages/store/src/-private/legacy-model-support/record-reference.ts @@ -1,15 +1,14 @@ -import { assert } from '@ember/debug'; -import { tracked } from '@glimmer/tracking'; - +import { defineSignal } from '@ember-data/tracking/-private'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; /** @module @ember-data/store */ -import type { SingleResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import type { NotificationType } from '../managers/notification-manager'; -import type Store from '../store-service'; +import type { Store } from '../store-service'; /** @module @ember-data/store @@ -21,15 +20,14 @@ import type Store from '../store-service'; @class RecordReference @public - @extends Reference */ export default class RecordReference { declare store: Store; // unsubscribe token given to us by the notification manager - ___token!: Object; + ___token!: object; ___identifier: StableRecordIdentifier; - @tracked _ref = 0; + declare _ref: number; constructor(store: Store, identifier: StableRecordIdentifier) { this.store = store; @@ -71,6 +69,7 @@ export default class RecordReference { @return {String} The id of the record. */ id() { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this._ref; // consume the tracked prop return this.___identifier.id; } @@ -157,7 +156,7 @@ export default class RecordReference { @param objectOrPromise a JSON:API ResourceDocument or a promise resolving to one @return a promise for the value (record or relationship) */ - push(objectOrPromise: SingleResourceDocument | Promise): Promise { + push(objectOrPromise: SingleResourceDocument | Promise): Promise { // TODO @deprecate pushing unresolved payloads return Promise.resolve(objectOrPromise).then((data) => { return this.store.push(data); @@ -181,7 +180,7 @@ export default class RecordReference { @public @return {Model} the record for this RecordReference */ - value(): RecordInstance | null { + value(): OpaqueRecordInstance | null { return this.store.peekRecord(this.___identifier); } @@ -235,3 +234,5 @@ export default class RecordReference { assert(`Unable to fetch record of type ${this.type} without an id`); } } + +defineSignal(RecordReference.prototype, '_ref'); diff --git a/packages/store/src/-private/legacy-model-support/schema-definition-service.ts b/packages/store/src/-private/legacy-model-support/schema-definition-service.ts deleted file mode 100644 index 5bc2d80ddd7..00000000000 --- a/packages/store/src/-private/legacy-model-support/schema-definition-service.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { getOwner } from '@ember/application'; -import { deprecate } from '@ember/debug'; - -import { importSync } from '@embroider/macros'; - -import { DEPRECATE_STRING_ARG_SCHEMAS } from '@ember-data/deprecations'; -import type Model from '@ember-data/model'; -import type { ModelFactory } from '@ember-data/model/-private/model'; -import { HAS_MODEL_PACKAGE } from '@ember-data/packages'; -import type { RecordIdentifier } from '@ember-data/types/q/identifier'; -import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; - -import type Store from '../store-service'; -import normalizeModelName from '../utils/normalize-model-name'; - -type ModelForMixin = (store: Store, normalizedModelName: string) => Model | null; - -let _modelForMixin: ModelForMixin; -if (HAS_MODEL_PACKAGE) { - let _found; - _modelForMixin = function () { - if (!_found) { - _found = (importSync('@ember-data/model/-private') as typeof import('@ember-data/model/-private'))._modelForMixin; - } - return _found(...arguments); - }; -} - -export class DSModelSchemaDefinitionService { - declare store: Store; - declare _relationshipsDefCache; - declare _attributesDefCache; - - constructor(store: Store) { - this.store = store; - this._relationshipsDefCache = Object.create(null); - this._attributesDefCache = Object.create(null); - } - - // Following the existing RD implementation - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - let modelName, attributes; - if (DEPRECATE_STRING_ARG_SCHEMAS) { - if (typeof identifier === 'string') { - deprecate( - `attributesDefinitionFor 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' }, - } - ); - modelName = identifier; - } else { - modelName = identifier.type; - } - } else { - modelName = identifier.type; - } - - attributes = this._attributesDefCache[modelName]; - - if (attributes === undefined) { - let modelClass = this.store.modelFor(modelName); - let attributeMap = modelClass.attributes; - - attributes = Object.create(null); - attributeMap.forEach((meta, name) => (attributes[name] = meta)); - this._attributesDefCache[modelName] = attributes; - } - - return attributes; - } - - // Following the existing RD implementation - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - let modelName, relationships; - if (DEPRECATE_STRING_ARG_SCHEMAS) { - if (typeof identifier === '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' }, - } - ); - modelName = identifier; - } else { - modelName = identifier.type; - } - } else { - modelName = identifier.type; - } - - relationships = this._relationshipsDefCache[modelName]; - - if (relationships === undefined) { - let modelClass = this.store.modelFor(modelName) as typeof Model; - relationships = modelClass.relationshipsObject || null; - this._relationshipsDefCache[modelName] = relationships; - } - - return relationships; - } - - doesTypeExist(modelName: string): boolean { - let normalizedModelName = normalizeModelName(modelName); - let factory = getModelFactory(this.store, this.store._modelFactoryCache, normalizedModelName); - - return factory !== null; - } -} - -export function getModelFactory(store: Store, cache, normalizedModelName: string): ModelFactory | null { - let factory = cache[normalizedModelName]; - - if (!factory) { - let owner: any = getOwner(store); - factory = owner.factoryFor(`model:${normalizedModelName}`); - - if (HAS_MODEL_PACKAGE) { - if (!factory) { - //Support looking up mixins as base types for polymorphic relationships - factory = _modelForMixin(store, normalizedModelName); - } - } - - if (!factory) { - // we don't cache misses in case someone wants to register a missing model - return null; - } - - let klass = factory.class; - - if (klass.isModel) { - let hasOwnModelNameSet = klass.modelName && Object.prototype.hasOwnProperty.call(klass, 'modelName'); - if (!hasOwnModelNameSet) { - Object.defineProperty(klass, 'modelName', { value: normalizedModelName }); - } - } - - cache[normalizedModelName] = factory; - } - - return factory; -} diff --git a/packages/store/src/-private/legacy-model-support/shim-model-class.ts b/packages/store/src/-private/legacy-model-support/shim-model-class.ts index 4173cc570c5..2b32ad46a03 100644 --- a/packages/store/src/-private/legacy-model-support/shim-model-class.ts +++ b/packages/store/src/-private/legacy-model-support/shim-model-class.ts @@ -1,91 +1,105 @@ -import type { ModelSchema } from '@ember-data/types/q/ds-model'; -import type { AttributeSchema, RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; -import type { Dict } from '@ember-data/types/q/utils'; +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; -import type Store from '../store-service'; +import type { KeyOrString, ModelSchema } from '../../-types/q/ds-model'; +import type { Store } from '../store-service'; // if modelFor turns out to be a bottleneck we should replace with a Map // and clear it during store teardown. -const AvailableShims = new WeakMap>(); +const AvailableShims = getOrSetGlobal('AvailableShims', new WeakMap>()); -export function getShimClass(store: Store, modelName: string): ShimModelClass { +export function getShimClass( + store: Store, + modelName: T extends TypedRecordInstance ? TypeFromInstance : string +): ShimModelClass { let shims = AvailableShims.get(store); if (!shims) { - shims = Object.create(null) as Dict; + shims = Object.create(null) as Record; AvailableShims.set(store, shims); } let shim = shims[modelName]; if (shim === undefined) { - shim = shims[modelName] = new ShimModelClass(store, modelName); + shim = shims[modelName] = new ShimModelClass(store, modelName); } return shim; } -function mapFromHash(hash: Dict): Map { - let map = new Map(); - for (let i in hash) { - if (Object.prototype.hasOwnProperty.call(hash, i)) { - map.set(i, hash[i]); - } - } - return map; -} - -// Mimics the static apis of DSModel -export default class ShimModelClass implements ModelSchema { +// Mimics the static apis of @ember-data/model +export default class ShimModelClass implements ModelSchema { declare __store: Store; - declare modelName: string; - constructor(store: Store, modelName: string) { + declare modelName: T extends TypedRecordInstance ? TypeFromInstance : string; + constructor(store: Store, modelName: T extends TypedRecordInstance ? TypeFromInstance : string) { this.__store = store; this.modelName = modelName; } - get fields(): Map { - let attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - let relationships = this.__store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName }); - let fields = new Map(); - Object.keys(attrs).forEach((key) => fields.set(key, 'attribute')); - Object.keys(relationships).forEach((key) => fields.set(key, relationships[key]!.kind)); + get fields(): Map, 'attribute' | 'belongsTo' | 'hasMany'> { + const fields = new Map, 'attribute' | 'belongsTo' | 'hasMany'>(); + const fieldSchemas = this.__store.schema.fields({ type: this.modelName }); + + fieldSchemas.forEach((schema, key) => { + if (schema.kind === 'attribute' || schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + fields.set(key as KeyOrString, schema.kind); + } + }); + return fields; } - get attributes(): Map { - let attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - return mapFromHash(attrs); + get attributes(): Map, LegacyAttributeField> { + const attrs = new Map, LegacyAttributeField>(); + const fields = this.__store.schema.fields({ type: this.modelName }); + + fields.forEach((schema, key) => { + if (schema.kind === 'attribute') { + attrs.set(key as KeyOrString, schema); + } + }); + + return attrs; } - get relationshipsByName(): Map { - let relationships = this.__store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName }); - return mapFromHash(relationships); + get relationshipsByName(): Map, LegacyRelationshipSchema> { + const rels = new Map, LegacyRelationshipSchema>(); + const fields = this.__store.schema.fields({ type: this.modelName }); + + fields.forEach((schema, key) => { + if (schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + rels.set(key as KeyOrString, schema); + } + }); + + return rels; } - eachAttribute(callback: (this: T | undefined, key: string, attribute: AttributeSchema) => void, binding?: T) { - let attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - Object.keys(attrDefs).forEach((key) => { - callback.call(binding, key, attrDefs[key] as AttributeSchema); + eachAttribute>(callback: (key: K, attribute: LegacyAttributeField) => void, binding?: T) { + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'attribute') { + callback.call(binding, key as K, schema); + } }); } - eachRelationship( - callback: (this: T | undefined, key: string, relationship: RelationshipSchema) => void, + eachRelationship>( + callback: (key: K, relationship: LegacyRelationshipSchema) => void, binding?: T ) { - let relationshipDefs = this.__store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: this.modelName }); - Object.keys(relationshipDefs).forEach((key) => { - callback.call(binding, key, relationshipDefs[key] as RelationshipSchema); + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + callback.call(binding, key as K, schema); + } }); } - eachTransformedAttribute(callback: (this: T | undefined, key: string, type: string | null) => void, binding?: T) { - const attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - Object.keys(attrDefs).forEach((key) => { - if (attrDefs[key]!.type) { - callback.call(binding, key, attrDefs[key]?.type ?? null); + eachTransformedAttribute>(callback: (key: K, type: string | null) => void, binding?: T) { + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'attribute') { + const type = schema.type; + if (type) callback.call(binding, key as K, type); } }); } diff --git a/packages/store/src/-private/managers/cache-capabilities-manager.ts b/packages/store/src/-private/managers/cache-capabilities-manager.ts new file mode 100644 index 00000000000..d614eca9799 --- /dev/null +++ b/packages/store/src/-private/managers/cache-capabilities-manager.ts @@ -0,0 +1,120 @@ +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; + +import type { CacheCapabilitiesManager as StoreWrapper } from '../../-types/q/cache-capabilities-manager'; +import type { SchemaService } from '../../-types/q/schema-service'; +import type { IdentifierCache } from '../caches/identifier-cache'; +import { isDocumentIdentifier, isStableIdentifier } from '../caches/identifier-cache'; +import type { Store } from '../store-service'; +import type { NotificationType } from './notification-manager'; + +/** + @module @ember-data/store +*/ + +export interface CacheCapabilitiesManager { + getSchemaDefinitionService(): SchemaService; +} +export class CacheCapabilitiesManager implements StoreWrapper { + declare _willNotify: boolean; + declare _pendingNotifies: Map>; + declare _store: Store; + + constructor(_store: Store) { + this._store = _store; + this._willNotify = false; + this._pendingNotifies = new Map(); + } + + get identifierCache(): IdentifierCache { + return this._store.identifierCache; + } + + _scheduleNotification(identifier: StableRecordIdentifier, key: string) { + let pending = this._pendingNotifies.get(identifier); + + if (!pending) { + pending = new Set(); + this._pendingNotifies.set(identifier, pending); + } + pending.add(key); + + if (this._willNotify === true) { + return; + } + + this._willNotify = true; + // it's possible a cache adhoc notifies us, + // in which case we sync flush + if (this._store._cbs) { + this._store._schedule('notify', () => this._flushNotifications()); + } else { + // TODO @runspired determine if relationship mutations should schedule + // into join/run vs immediate flush + this._flushNotifications(); + } + } + + _flushNotifications(): void { + if (this._willNotify === false) { + return; + } + + const pending = this._pendingNotifies; + this._pendingNotifies = new Map(); + this._willNotify = false; + + pending.forEach((set, identifier) => { + set.forEach((key) => { + this._store.notifications.notify(identifier, 'relationships', key); + }); + }); + } + + 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 + ): void { + assert(`Expected a stable identifier`, isStableIdentifier(identifier) || isDocumentIdentifier(identifier)); + + // TODO do we still get value from this? + if (namespace === 'relationships' && key) { + this._scheduleNotification(identifier as StableRecordIdentifier, key); + return; + } + + // @ts-expect-error + this._store.notifications.notify(identifier, namespace, key); + } + + get schema() { + return this._store.schema; + } + + setRecordId(identifier: StableRecordIdentifier, id: string) { + assert(`Expected a stable identifier`, isStableIdentifier(identifier)); + this._store._instanceCache.setRecordId(identifier, id); + } + + hasRecord(identifier: StableRecordIdentifier): boolean { + return Boolean(this._store._instanceCache.peek(identifier)); + } + + disconnectRecord(identifier: StableRecordIdentifier): void { + assert(`Expected a stable identifier`, isStableIdentifier(identifier)); + this._store._instanceCache.disconnect(identifier); + this._pendingNotifies.delete(identifier); + } +} + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + CacheCapabilitiesManager.prototype.getSchemaDefinitionService = function () { + // FIXME add deprecation for this + return this._store.schema; + }; +} diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index b03daba596c..8c335c2b8b8 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -1,72 +1,15 @@ -import { assert, deprecate } from '@ember/debug'; - -import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import type { StructuredDataDocument } from '@ember-data/request/-private/types'; -import type { Change } from '@ember-data/types/cache/change'; -import type { - ResourceDocument, - SingleResourceDataDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; -import type { - CollectionResourceRelationship, - JsonApiDocument, - SingleResourceDocument, - SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { Dict } from '@ember-data/types/q/utils'; - -import type { StoreRequestContext } from '../cache-handler'; -import { isStableIdentifier } from '../caches/identifier-cache'; -import type Store from '../store-service'; - -export function legacyCachePut( - store: Store, - doc: StructuredDataDocument | { content: JsonApiDocument } -): ResourceDocument { - const jsonApiDoc = doc.content; - let ret: ResourceDocument; - store._join(() => { - let included = jsonApiDoc.included; - let i: number, length: number; - - if (included) { - for (i = 0, length = included.length; i < length; i++) { - store._instanceCache.loadData(included[i]); - } - } - - if (Array.isArray(jsonApiDoc.data)) { - length = jsonApiDoc.data.length; - let identifiers: StableExistingRecordIdentifier[] = []; - - for (i = 0; i < length; i++) { - identifiers.push(store._instanceCache.loadData(jsonApiDoc.data[i])); - } - ret = { data: identifiers }; - return; - } - - if (jsonApiDoc.data === null) { - ret = { data: null }; - return; - } - - assert( - `Expected an object in the 'data' property in a call to 'push', but was ${typeof jsonApiDoc.data}`, - typeof jsonApiDoc.data === 'object' - ); - - ret = { data: store._instanceCache.loadData(jsonApiDoc.data) }; - return; - }); - - return ret!; -} +import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { Change } from '@warp-drive/core-types/cache/change'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { StructuredDataDocument, StructuredDocument } from '@warp-drive/core-types/request'; +import type { ResourceDocument, SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; + +import type { StoreRequestContext } from '../cache-handler/handler'; /** * The CacheManager wraps a Cache enforcing that only @@ -75,74 +18,24 @@ export function legacyCachePut( * Hence, it is the value of `Store.cache`, wrapping * the cache instance returned by `Store.createCache`. * - * This class is the the return value of both the - * `recordDataFor` function supplied to the store - * hook `instantiateRecord`, and the `recordDataFor` - * method on the `CacheStoreWrapper`. It is not - * directly instantiable. - * * It handles translating between cache versions when * necessary, for instance when a Store is configured * to use both a v1 and a v2 cache depending on some * heuristic. * * Starting with the v2 spec, the cache is designed such - * that it must be implemented as a singleton. However, - * because the v1 spec was not designed for this whenever - * we encounter any v1 cache we must wrap all caches, even - * singletons, in non-singleton managers to preserve v1 - * compatibility. - * - * To avoid this performance penalty being paid by all - * applications, singleton behavior may be opted-in via - * the configuration supplied to your Ember application - * at build time. This effectively removes support for - * v1 caches. - * - * ```js - * let app = new EmberApp(defaults, { - * emberData: { - * useSingletonManager: true - * }, - * }); - * ``` + * that it must be implemented as a singleton. * * @class CacheManager * @public */ -export class NonSingletonCacheManager implements Cache { - version: '2' = '2'; - - #store: Store; - #cache: Cache | CacheV1; - #identifier: StableRecordIdentifier; +export class CacheManager implements Cache { + version = '2' as const; - get managedVersion() { - return this.#cache.version || '1'; - } + #cache: Cache; - constructor(store: Store, cache: Cache | CacheV1, identifier: StableRecordIdentifier) { - this.#store = store; + constructor(cache: Cache) { this.#cache = cache; - this.#identifier = identifier; - - if (this.#isDeprecated(cache)) { - deprecate( - `This RecordData uses the deprecated V1 RecordData Spec. Upgrade to V2 to maintain compatibility.`, - false, - { - id: 'ember-data:deprecate-v1-cache', - until: '5.0', - since: { available: '4.7', enabled: '4.7' }, - for: 'ember-data', - } - ); - } - } - - #isDeprecated(cache: Cache | CacheV1): cache is CacheV1 { - let version = cache.version || '1'; - return version !== this.version; } // Cache Management @@ -168,19 +61,11 @@ export class NonSingletonCacheManager implements Cache { * * @method put * @param {StructuredDocument} doc - * @returns {ResourceDocument} + * @return {ResourceDocument} * @public */ put(doc: StructuredDocument | { content: T }): ResourceDocument { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - if (doc instanceof Error) { - // in legacy we don't know how to handle this - throw doc; - } - return legacyCachePut(this.#store, doc as { content: JsonApiDocument }); - } - return cache.put(doc); + return this.#cache.put(doc); } /** @@ -192,14 +77,10 @@ export class NonSingletonCacheManager implements Cache { * @method patch * @public * @param op the operation to perform - * @returns {void} + * @return {void} */ patch(op: MergeOperation): void { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - return; - } - cache.patch(op); + this.#cache.patch(op); } /** @@ -210,50 +91,8 @@ export class NonSingletonCacheManager implements Cache { * @public * @param mutation */ - // isResource is only needed for interop with v1 cache - mutate(mutation: LocalRelationshipOperation, isResource?: boolean): void { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - const instanceCache = this.#store._instanceCache; - switch (mutation.op) { - case 'addToRelatedRecords': - cache.addToHasMany( - mutation.field, - (mutation.value as StableRecordIdentifier[]).map((i) => instanceCache.getResourceCache(i)), - mutation.index - ); - return; - case 'removeFromRelatedRecords': - cache.removeFromHasMany( - mutation.field, - (mutation.value as StableRecordIdentifier[]).map((i) => instanceCache.getResourceCache(i)) - ); - return; - case 'replaceRelatedRecords': - cache.setDirtyHasMany( - mutation.field, - mutation.value.map((i) => instanceCache.getResourceCache(i)) - ); - return; - case 'replaceRelatedRecord': - if (isResource) { - cache.setDirtyBelongsTo( - mutation.field, - mutation.value ? instanceCache.getResourceCache(mutation.value) : null - ); - return; - } - cache.removeFromHasMany(mutation.field, [instanceCache.getResourceCache(mutation.prior!)]); - cache.addToHasMany(mutation.field, [instanceCache.getResourceCache(mutation.value!)], mutation.index); - return; - case 'sortRelatedRecords': - return; - default: - return; - } - } else { - cache.mutate(mutation); - } + mutate(mutation: LocalRelationshipOperation): void { + this.#cache.mutate(mutation); } /** @@ -286,16 +125,12 @@ export class NonSingletonCacheManager implements Cache { * @method peek * @public * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier - * @returns {ResourceDocument | ResourceBlob | null} the known resource data + * @return {ResourceDocument | ResourceBlob | null} the known resource data */ peek(identifier: StableRecordIdentifier): unknown; peek(identifier: StableDocumentIdentifier): ResourceDocument | null; peek(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement peek`); - } - return cache.peek(identifier); + return this.#cache.peek(identifier); } /** @@ -304,15 +139,11 @@ export class NonSingletonCacheManager implements Cache { * * @method peekRequest * @param {StableDocumentIdentifier} - * @returns {StableDocumentIdentifier | null} + * @return {StableDocumentIdentifier | null} * @public */ peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement peekRequest`); - } - return cache.peekRequest(identifier); + return this.#cache.peekRequest(identifier); } /** @@ -323,20 +154,10 @@ export class NonSingletonCacheManager implements Cache { * @param identifier * @param data * @param hasRecord - * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned + * @return {void | string[]} if `hasRecord` is true then calculated key changes should be returned */ - upsert(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord: boolean): void | string[] { - const cache = this.#cache; - // called by something V1 - if (!isStableIdentifier(identifier)) { - data = identifier as JsonApiResource; - hasRecord = data as unknown as boolean; - identifier = this.#identifier; - } - if (this.#isDeprecated(cache)) { - return cache.pushData(data, hasRecord); - } - return cache.upsert(identifier, data, hasRecord); + upsert(identifier: StableRecordIdentifier, data: unknown, hasRecord: boolean): void | string[] { + return this.#cache.upsert(identifier, data, hasRecord); } // Cache Forking Support @@ -351,14 +172,10 @@ export class NonSingletonCacheManager implements Cache { * * @method fork * @public - * @returns Promise + * @return Promise */ fork(): Promise { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement fork`); - } - return cache.fork(); + return this.#cache.fork(); } /** @@ -371,14 +188,10 @@ export class NonSingletonCacheManager implements Cache { * @method merge * @param {Cache} cache * @public - * @returns Promise + * @return Promise */ - merge(updates: Cache): Promise { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement merge`); - } - return cache.merge(updates); + merge(cache: Cache): Promise { + return this.#cache.merge(cache); } /** @@ -415,11 +228,7 @@ export class NonSingletonCacheManager implements Cache { * @public */ diff(): Promise { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement diff`); - } - return cache.diff(); + return this.#cache.diff(); } // SSR Support @@ -431,15 +240,11 @@ export class NonSingletonCacheManager implements Cache { * via `cache.hydrate`. * * @method dump - * @returns {Promise} + * @return {Promise} * @public */ dump(): Promise> { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement dump`); - } - return cache.dump(); + return this.#cache.dump(); } /** @@ -456,49 +261,16 @@ export class NonSingletonCacheManager implements Cache { * * @method hydrate * @param {ReadableStream} stream - * @returns {Promise} + * @return {Promise} * @public */ hydrate(stream: ReadableStream): Promise { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - throw new Error(`Expected cache to implement hydrate`); - } - return cache.hydrate(stream); + return this.#cache.hydrate(stream); } // Cache // ===== - /** - * Retrieve the identifier for this v1 cache - * - * DEPRECATED Caches should not be assumed to be 1:1 with resources - * - * @method getResourceIdentifier - * @public - * @deprecated - */ - getResourceIdentifier(): StableRecordIdentifier { - return this.#identifier; - } - - /** - * Push resource data from a remote source into the cache for this identifier - * - * DEPRECATED Use upsert. Caches should not be assumed to be 1:1 with resources - * - * @method pushData - * @param data - * @param hasRecord - * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned - * @public - * @deprecated - */ - pushData(data: JsonApiResource, hasRecord: boolean): void | string[] { - return this.upsert(this.#identifier, data, hasRecord); - } - // Resource Support // ================ @@ -513,41 +285,8 @@ export class NonSingletonCacheManager implements Cache { * @param identifier * @param options */ - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict): Dict { - // called by something V1 - if (!isStableIdentifier(identifier)) { - options = identifier; - identifier = this.#identifier; - } - const cache = this.#cache; - - // TODO deprecate return value - if (this.#isDeprecated(cache)) { - cache.clientDidCreate(); - // if a V2 is calling a V1 we need to call both methods - return cache._initRecordCreateOptions(options); - } else { - return cache.clientDidCreate(identifier, options); - } - } - - /** - * Pass options to the cache that were supplied to a new record - * instantiated on the client. - * - * DEPRECATED: options are now passed via `clientDidCreate` - * - * @method clientDidCreate - * @public - * @deprecated - * @param options - */ - _initRecordCreateOptions(options?: Dict) { - const cache = this.#cache; - - if (this.#isDeprecated(cache)) { - return cache._initRecordCreateOptions(options); - } + clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { + return this.#cache.clientDidCreate(identifier, options); } /** @@ -559,19 +298,7 @@ export class NonSingletonCacheManager implements Cache { * @param identifier */ willCommit(identifier: StableRecordIdentifier, context: StoreRequestContext): void { - // called by something V1 - if (!isStableIdentifier(identifier)) { - identifier = this.#identifier; - } - const cache = this.#cache; - - // TODO deprecate return value - if (this.#isDeprecated(cache)) { - cache.willCommit(); - } else { - assert(`Cannot call a v2 cache willCommit from a v1 cache`, !!context); - cache.willCommit(identifier, context); - } + this.#cache.willCommit(identifier, context); } /** @@ -584,23 +311,7 @@ export class NonSingletonCacheManager implements Cache { * @param data */ didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - // called by something V1 - if (!isStableIdentifier(identifier)) { - cache.didCommit(identifier); - return { data: this.#identifier as StableExistingRecordIdentifier }; - } - cache.didCommit((result.content as SingleResourceDocument)?.data); - return { data: this.#identifier as StableExistingRecordIdentifier }; - } else { - // called by something V1 - if (!isStableIdentifier(identifier)) { - cache.didCommit(this.#identifier, { content: { data: identifier } }); - return { data: this.#identifier as StableExistingRecordIdentifier }; - } - return cache.didCommit(identifier, result); - } + return this.#cache.didCommit(identifier, result); } /** @@ -612,8 +323,8 @@ export class NonSingletonCacheManager implements Cache { * @param identifier * @param errors */ - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[]) { - this.#cache.commitWasRejected(identifier || this.#identifier, errors); + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + this.#cache.commitWasRejected(identifier, errors); } /** @@ -625,12 +336,7 @@ export class NonSingletonCacheManager implements Cache { * @param identifier */ unloadRecord(identifier: StableRecordIdentifier): void { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - cache.unloadRecord(); - } else { - cache.unloadRecord(identifier || this.#identifier); - } + this.#cache.unloadRecord(identifier); } // Granular Resource Data APIs @@ -643,16 +349,10 @@ export class NonSingletonCacheManager implements Cache { * @public * @param identifier * @param propertyName - * @returns {unknown} + * @return {unknown} */ - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - // called by something V1 - if (!isStableIdentifier(identifier)) { - propertyName = identifier; - identifier = this.#identifier; - } - const cache = this.#cache; - return this.#isDeprecated(cache) ? cache.getAttr(propertyName) : cache.getAttr(identifier, propertyName); + getAttr(identifier: StableRecordIdentifier, propertyName: string): Value | undefined { + return this.#cache.getAttr(identifier, propertyName); } /** @@ -664,51 +364,8 @@ export class NonSingletonCacheManager implements Cache { * @param propertyName * @param value */ - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.setDirtyAttribute(propertyName, value) - : cache.setAttr(identifier, propertyName, value); - } - - /** - * Mutate the data for an attribute in the cache - * - * DEPRECATED use setAttr - * - * @method setDirtyAttribute - * @public - * @deprecated - * @param identifier - * @param propertyName - * @param value - */ - setDirtyAttribute(propertyName: string, value: unknown): void { - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.setDirtyAttribute(propertyName, value) - : cache.setAttr(this.#identifier, propertyName, value); - } - - /** - * Query the cache for the changed attributes of a resource. - * - * DEPRECATED use changedAttrs - * - * @method changedAttributes - * @public - * @deprecated - * @param identifier - * @returns - */ - changedAttributes(): ChangedAttributesHash { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - return cache.changedAttributes(); - } - return cache.changedAttrs(this.#identifier); + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: Value): void { + this.#cache.setAttr(identifier, propertyName, value); } /** @@ -716,31 +373,11 @@ export class NonSingletonCacheManager implements Cache { * * @method changedAttrs * @public - * @deprecated * @param identifier - * @returns + * @return */ changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - const cache = this.#cache; - if (this.#isDeprecated(cache)) { - return cache.changedAttributes(); - } - return cache.changedAttrs(identifier); - } - - /** - * Query the cache for whether any mutated attributes exist - * - * DEPRECATED use hasChangedAttrs - * - * @method hasChangedAttributes - * @public - * @deprecated - * @returns {boolean} - */ - hasChangedAttributes(): boolean { - const cache = this.#cache; - return this.#isDeprecated(cache) ? cache.hasChangedAttributes() : cache.hasChangedAttrs(this.#identifier); + return this.#cache.changedAttrs(identifier); } /** @@ -749,26 +386,10 @@ export class NonSingletonCacheManager implements Cache { * @method hasChangedAttrs * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - const cache = this.#cache; - return this.#isDeprecated(cache) ? cache.hasChangedAttributes() : cache.hasChangedAttrs(identifier); - } - - /** - * Tell the cache to discard any uncommitted mutations to attributes - * - * DEPRECATED use rollbackAttrs - * - * @method rollbackAttributes - * @public - * @deprecated - * @returns - */ - rollbackAttributes() { - const cache = this.#cache; - return this.#isDeprecated(cache) ? cache.rollbackAttributes() : cache.rollbackAttrs(this.#identifier); + return this.#cache.hasChangedAttrs(identifier); } /** @@ -777,189 +398,88 @@ export class NonSingletonCacheManager implements Cache { * @method rollbackAttrs * @public * @param identifier - * @returns the names of attributes that were restored + * @return the names of attributes that were restored */ rollbackAttrs(identifier: StableRecordIdentifier): string[] { - const cache = this.#cache; - return this.#isDeprecated(cache) ? cache.rollbackAttributes() : cache.rollbackAttrs(identifier); + return this.#cache.rollbackAttrs(identifier); } // Relationships // ============= - // the third arg here is "private". In a world with only V2 it is not necessary - // but in one in which we must convert a call from V2 -> V1 it is required to do this - // or else to do nasty schema lookup things - // @runspired has implemented this concept in relationships spikes and is confident - // we do not need any signal about whether a relationship is a collection or not at this - // boundary /** - * Query the cache for the current state of a relationship property + * Query the cache for the changes to relationships of a resource. * - * @method getRelationship - * @public - * @param identifier - * @param propertyName - * @returns resource relationship object - */ - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string, - isCollection = false - ): SingleResourceRelationship | CollectionResourceRelationship { - const cache = this.#cache; - - if (this.#isDeprecated(cache)) { - let isBelongsTo = !isCollection; - return isBelongsTo ? cache.getBelongsTo(propertyName) : cache.getHasMany(propertyName); - } - - return cache.getRelationship(identifier, propertyName); - } - - /** - * Query the cache for the current state of a belongsTo field + * Returns a map of relationship names to RelationshipDiff objects. * - * DEPRECATED use `getRelationship` - * - * @method getBelongsTo - * @public - * @deprecated - * @param propertyName - * @returns single resource relationship object - */ - getBelongsTo(propertyName: string): SingleResourceRelationship { - const cache = this.#cache; - - if (this.#isDeprecated(cache)) { - return cache.getBelongsTo(propertyName); - } else { - let identifier = this.#identifier; - return cache.getRelationship(identifier, propertyName) as SingleResourceRelationship; + * ```ts + * type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; } - } - - /** - * Query the cache for the current state of a hasMany field + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + ``` * - * DEPRECATED use `getRelationship` - * - * @method getHasMany + * @method changedRelationships * @public - * @deprecated - * @param propertyName - * @returns single resource relationship object + * @param {StableRecordIdentifier} identifier + * @return {Map} */ - getHasMany(propertyName: string): CollectionResourceRelationship { - const cache = this.#cache; - - if (this.#isDeprecated(cache)) { - return cache.getHasMany(propertyName); - } else { - let identifier = this.#identifier; - return cache.getRelationship(identifier, propertyName) as CollectionResourceRelationship; - } + changedRelationships(identifier: StableRecordIdentifier): Map { + return this.#cache.changedRelationships(identifier); } /** - * Mutate the current state of a belongsTo relationship - * - * DEPRECATED use update + * Query the cache for whether any mutated attributes exist * - * @method setDirtyBelongsTo + * @method hasChangedRelationships * @public - * @deprecated - * @param propertyName - * @param value + * @param {StableRecordIdentifier} identifier + * @return {boolean} */ - setDirtyBelongsTo(propertyName: string, value: NonSingletonCacheManager | null) { - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.setDirtyBelongsTo(propertyName, value) - : cache.mutate({ - op: 'replaceRelatedRecord', - record: this.#identifier, - field: propertyName, - value: value ? value.getResourceIdentifier() : null, - }); + hasChangedRelationships(identifier: StableRecordIdentifier): boolean { + return this.#cache.hasChangedRelationships(identifier); } /** - * Mutate the current state of a hasMany relationship by adding values - * An index may optionally be specified which the cache should use for - * where in the list to insert the records - * - * DEPRECATED use update + * Tell the cache to discard any uncommitted mutations to relationships. * - * @method addToHasMany - * @deprecated - * @public - * @param propertyName - * @param value - * @param idx - */ - addToHasMany(propertyName: string, value: NonSingletonCacheManager[], idx?: number): void { - const identifier = this.#identifier; - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.addToHasMany(propertyName, value, idx) - : cache.mutate({ - op: 'addToRelatedRecords', - field: propertyName, - record: identifier, - value: value.map((v) => v.getResourceIdentifier()), - }); - } - - /** - * Mutate the current state of a hasMany relationship by removing values. + * This will also discard the change on any appropriate inverses. * - * DEPRECATED use update + * This method is a candidate to become a mutation * - * @method removeFromHasMany - * @deprecated + * @method rollbackRelationships * @public - * @param propertyName - * @param value + * @param {StableRecordIdentifier} identifier + * @return {string[]} the names of relationships that were restored */ - removeFromHasMany(propertyName: string, value: Cache[]): void { - const identifier = this.#identifier; - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.removeFromHasMany(propertyName, value) - : cache.mutate({ - op: 'removeFromRelatedRecords', - record: identifier, - field: propertyName, - value: (value as unknown as NonSingletonCacheManager[]).map((v) => v.getResourceIdentifier()), - }); + rollbackRelationships(identifier: StableRecordIdentifier): string[] { + return this.#cache.rollbackRelationships(identifier); } /** - * Mutate the current state of a hasMany relationship by replacing it entirely - * - * DEPRECATED use `setHasMany` + * Query the cache for the current state of a relationship property * - * @method setDirtyHasMany + * @method getRelationship * @public - * @deprecated + * @param identifier * @param propertyName - * @param value + * @return resource relationship object */ - setDirtyHasMany(propertyName: string, value: NonSingletonCacheManager[]) { - const cache = this.#cache; - - this.#isDeprecated(cache) - ? cache.setDirtyHasMany(propertyName, value) - : cache.mutate({ - op: 'replaceRelatedRecords', - record: this.#identifier, - field: propertyName, - value: value.map((rd) => rd.getResourceIdentifier()), - }); + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + return this.#cache.getRelationship(identifier, propertyName); } // Resource State @@ -975,12 +495,7 @@ export class NonSingletonCacheManager implements Cache { * @param isDeleted */ setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - if (!isStableIdentifier(identifier)) { - isDeleted = identifier as boolean; - identifier = this.#identifier; - } - const cache = this.#cache; - this.#isDeprecated(cache) ? cache.setIsDeleted(isDeleted) : cache.setIsDeleted(identifier, isDeleted); + this.#cache.setIsDeleted(identifier, isDeleted); } /** @@ -989,10 +504,10 @@ export class NonSingletonCacheManager implements Cache { * @method getErrors * @public * @param identifier - * @returns + * @return */ - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { - return this.#cache.getErrors(identifier || this.#identifier); + getErrors(identifier: StableRecordIdentifier): ApiError[] { + return this.#cache.getErrors(identifier); } /** @@ -1001,13 +516,10 @@ export class NonSingletonCacheManager implements Cache { * @method isEmpty * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isEmpty(identifier: StableRecordIdentifier): boolean { - const cache = this.#cache; - return this.#isDeprecated(cache) - ? cache.isEmpty?.(identifier || this.#identifier) || false - : cache.isEmpty(identifier || this.#identifier); + return this.#cache.isEmpty(identifier); } /** @@ -1017,10 +529,10 @@ export class NonSingletonCacheManager implements Cache { * @method isNew * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isNew(identifier: StableRecordIdentifier): boolean { - return this.#cache.isNew(identifier || this.#identifier); + return this.#cache.isNew(identifier); } /** @@ -1030,10 +542,10 @@ export class NonSingletonCacheManager implements Cache { * @method isDeleted * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ isDeleted(identifier: StableRecordIdentifier): boolean { - return this.#cache.isDeleted(identifier || this.#identifier); + return this.#cache.isDeleted(identifier); } /** @@ -1043,138 +555,8 @@ export class NonSingletonCacheManager implements Cache { * @method isDeletionCommitted * @public * @param identifier - * @returns {boolean} + * @return {boolean} */ - isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return this.#cache.isDeletionCommitted(identifier || this.#identifier); - } -} - -export class SingletonCacheManager implements Cache { - version: '2' = '2'; - - #cache: Cache; - - constructor(cache: Cache) { - this.#cache = cache; - } - - put(doc: StructuredDocument): ResourceDocument { - return this.#cache.put(doc); - } - - peek(identifier: StableRecordIdentifier): unknown; - peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - peek(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { - return this.#cache.peek(identifier); - } - peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { - return this.#cache.peekRequest(identifier); - } - - fork(): Promise { - return this.#cache.fork(); - } - merge(cache: Cache): Promise { - return this.#cache.merge(cache); - } - diff(): Promise { - return this.#cache.diff(); - } - dump(): Promise> { - return this.#cache.dump(); - } - hydrate(stream: ReadableStream): Promise { - return this.#cache.hydrate(stream); - } - - // Cache - // ===== - - upsert(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord: boolean): void | string[] { - return this.#cache.upsert(identifier, data, hasRecord); - } - - patch(op: MergeOperation): void { - this.#cache.patch(op); - } - - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict): Dict { - return this.#cache.clientDidCreate(identifier, options); - } - - willCommit(identifier: StableRecordIdentifier, context: StoreRequestContext): void { - this.#cache.willCommit(identifier, context); - } - - didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { - return this.#cache.didCommit(identifier, result); - } - - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[]): void { - this.#cache.commitWasRejected(identifier, errors); - } - - unloadRecord(identifier: StableRecordIdentifier): void { - this.#cache.unloadRecord(identifier); - } - - // Attrs - // ===== - - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - return this.#cache.getAttr(identifier, propertyName); - } - - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - this.#cache.setAttr(identifier, propertyName, value); - } - - changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - return this.#cache.changedAttrs(identifier); - } - - hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - return this.#cache.hasChangedAttrs(identifier); - } - - rollbackAttrs(identifier: StableRecordIdentifier): string[] { - return this.#cache.rollbackAttrs(identifier); - } - - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string - ): SingleResourceRelationship | CollectionResourceRelationship { - return this.#cache.getRelationship(identifier, propertyName); - } - mutate(mutation: LocalRelationshipOperation): void { - this.#cache.mutate(mutation); - } - - // State - // ============= - - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - this.#cache.setIsDeleted(identifier, isDeleted); - } - - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { - return this.#cache.getErrors(identifier); - } - - isEmpty(identifier: StableRecordIdentifier): boolean { - return this.#cache.isEmpty(identifier); - } - - isNew(identifier: StableRecordIdentifier): boolean { - return this.#cache.isNew(identifier); - } - - isDeleted(identifier: StableRecordIdentifier): boolean { - return this.#cache.isDeleted(identifier); - } - isDeletionCommitted(identifier: StableRecordIdentifier): boolean { return this.#cache.isDeletionCommitted(identifier); } diff --git a/packages/store/src/-private/managers/cache-store-wrapper.ts b/packages/store/src/-private/managers/cache-store-wrapper.ts deleted file mode 100644 index 95cae717a4e..00000000000 --- a/packages/store/src/-private/managers/cache-store-wrapper.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { assert, deprecate } from '@ember/debug'; - -import { DEPRECATE_V1_RECORD_DATA, DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/deprecations'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache } from '@ember-data/types/q/cache'; -import type { - LegacyCacheStoreWrapper, - V2CacheStoreWrapper as StoreWrapper, -} from '@ember-data/types/q/cache-store-wrapper'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; -import { SchemaService } from '@ember-data/types/q/schema-service'; - -import { IdentifierCache, isDocumentIdentifier, isStableIdentifier } from '../caches/identifier-cache'; -import type Store from '../store-service'; -import coerceId from '../utils/coerce-id'; -import constructResource from '../utils/construct-resource'; -import normalizeModelName from '../utils/normalize-model-name'; -import { NotificationType } from './notification-manager'; - -/** - @module @ember-data/store -*/ - -class LegacyWrapper implements LegacyCacheStoreWrapper { - declare _willNotify: boolean; - declare _pendingNotifies: Map>; - declare _store: Store; - - constructor(_store: Store) { - this._store = _store; - this._willNotify = false; - this._pendingNotifies = new Map(); - } - - get identifierCache(): IdentifierCache { - return this._store.identifierCache; - } - - _scheduleNotification(identifier: StableRecordIdentifier, key: string) { - let pending = this._pendingNotifies.get(identifier); - - if (!pending) { - pending = new Set(); - this._pendingNotifies.set(identifier, pending); - } - pending.add(key); - - if (this._willNotify === true) { - return; - } - - this._willNotify = true; - // it's possible a RecordData adhoc notifies us, - // in which case we sync flush - if (this._store._cbs) { - this._store._schedule('notify', () => this._flushNotifications()); - } else { - this._flushNotifications(); - } - } - - _flushNotifications(): void { - if (this._willNotify === false) { - return; - } - - let pending = this._pendingNotifies; - this._pendingNotifies = new Map(); - this._willNotify = false; - - pending.forEach((set, identifier) => { - set.forEach((key) => { - this._store.notifications.notify(identifier, 'relationships', key); - }); - }); - } - - notifyChange(identifier: StableRecordIdentifier, namespace: 'added' | 'removed'): void; - notifyChange(identifier: StableRecordIdentifier, namespace: NotificationType, key?: string): void; - notifyChange( - identifier: StableRecordIdentifier, - namespace: NotificationType | 'added' | 'removed', - key?: string - ): void { - assert(`Expected a stable identifier`, isStableIdentifier(identifier) || isDocumentIdentifier(identifier)); - - // TODO do we still get value from this? - if (namespace === 'relationships' && key) { - this._scheduleNotification(identifier, key); - return; - } - - // @ts-expect-error - this._store.notifications.notify(identifier, namespace, key); - } - - notifyErrorsChange(type: string, id: string, lid: string | null): void; - notifyErrorsChange(type: string, id: string | null, lid: string): void; - notifyErrorsChange(type: string, id: string | null, lid: string | null): void { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.notifyErrorsChange has been deprecated in favor of StoreWrapper.notifyChange`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - - this._store.notifications.notify(identifier, 'errors'); - } - - attributesDefinitionFor(type: string): AttributesSchema { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate( - `StoreWrapper.attributesDefinitionFor has been deprecated in favor of StoreWrapper.getSchemaDefinitionService().attributesDefinitionFor`, - false, - { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - } - ); - } - return this._store.getSchemaDefinitionService().attributesDefinitionFor({ type }); - } - - relationshipsDefinitionFor(type: string): RelationshipsSchema { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate( - `StoreWrapper.relationshipsDefinitionFor has been deprecated in favor of StoreWrapper.getSchemaDefinitionService().relationshipsDefinitionFor`, - false, - { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - } - ); - } - return this._store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); - } - - getSchemaDefinitionService(): SchemaService { - return this._store.getSchemaDefinitionService(); - } - - notifyPropertyChange(type: string, id: string | null, lid: string, key?: string): void; - notifyPropertyChange(type: string, id: string, lid: string | null | undefined, key?: string): void; - notifyPropertyChange(type: string, id: string | null, lid: string | null | undefined, key?: string): void { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.notifyPropertyChange has been deprecated in favor of StoreWrapper.notifyChange`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - - this._store.notifications.notify(identifier, 'attributes', key); - } - - notifyHasManyChange(type: string, id: string | null, lid: string, key: string): void; - notifyHasManyChange(type: string, id: string, lid: string | null | undefined, key: string): void; - notifyHasManyChange(type: string, id: string | null, lid: string | null | undefined, key: string): void { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.notifyHasManyChange has been deprecated in favor of StoreWrapper.notifyChange`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - this._scheduleNotification(identifier, key); - } - - notifyBelongsToChange(type: string, id: string | null, lid: string, key: string): void; - notifyBelongsToChange(type: string, id: string, lid: string | null | undefined, key: string): void; - notifyBelongsToChange(type: string, id: string | null, lid: string | null | undefined, key: string): void { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.notifyBelongsToChange has been deprecated in favor of StoreWrapper.notifyChange`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - - this._scheduleNotification(identifier, key); - } - - notifyStateChange(type: string, id: string, lid: string | null, key?: string): void; - notifyStateChange(type: string, id: string | null, lid: string, key?: string): void; - notifyStateChange(type: string, id: string | null, lid: string | null, key?: string): void { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.notifyStateChange has been deprecated in favor of StoreWrapper.notifyChange`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - - this._store.notifications.notify(identifier, 'state'); - } - - recordDataFor(type: string, id: string, lid?: string | null): Cache; - recordDataFor(type: string, id: string | null, lid: string): Cache; - recordDataFor(type: string): Cache; - recordDataFor(type: StableRecordIdentifier): Cache; - recordDataFor(type: string | StableRecordIdentifier, id?: string | null, lid?: string | null): Cache { - let identifier: StableRecordIdentifier; - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (!isStableIdentifier(type)) { - // we also deprecate create capability. This behavior was problematic because - // there's no outside association between this RecordData and an Identifier. - // It's likely a mistake when we hit this code-path, but we said in an early - // RFC we'd allow this. - // With V2 we are enforcing someone to use the record-data and identifier-cache APIs to - // create a new identifier and then call clientDidCreate on the RecordData - // instead. - identifier = - !id && !lid - ? this.identifierCache.createIdentifierForNewRecord({ type: type }) - : this.identifierCache.getOrCreateRecordIdentifier(constructResource(type, id, lid)); - } else { - identifier = type; - } - } else { - assert(`Expected a stable identifier`, isStableIdentifier(type)); - identifier = type; - } - - const cache = DEPRECATE_V1_RECORD_DATA - ? this._store._instanceCache.getResourceCache(identifier) - : this._store.cache; - - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (!id && !lid && typeof type === 'string') { - cache.clientDidCreate(identifier); - this._store.recordArrayManager.identifierAdded(identifier); - } - } - - return cache; - } - - setRecordId(type: string | StableRecordIdentifier, id: string, lid?: string) { - let identifier: StableRecordIdentifier | undefined; - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (!isStableIdentifier(type)) { - const modelName = normalizeModelName(type); - const resource = constructResource(modelName, null, coerceId(lid)); - identifier = this.identifierCache.peekRecordIdentifier(resource); - } else { - identifier = type; - } - } else { - assert(`Expected a stable identifier`, isStableIdentifier(type)); - identifier = type; - } - - assert(`Unable to find an identifier to update the ID for for ${String(lid)}`, identifier); - - this._store._instanceCache.setRecordId(identifier, id); - } - - isRecordInUse(type: string, id: string | null, lid: string): boolean; - isRecordInUse(type: string, id: string, lid?: string | null): boolean; - isRecordInUse(type: string, id: string | null, lid?: string | null): boolean { - if (DEPRECATE_V1CACHE_STORE_APIS) { - deprecate(`StoreWrapper.isRecordInUSe has been deprecated in favor of StoreWrapper.hasRecord`, false, { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - }); - } - const resource = constructResource(type, id, lid); - const identifier = this.identifierCache.peekRecordIdentifier(resource); - - const record = identifier && this._store._instanceCache.peek({ identifier, bucket: 'record' }); - - return record ? !(record.isDestroyed || record.isDestroying) : false; - } - - hasRecord(identifier: StableRecordIdentifier): boolean { - return Boolean(this._store._instanceCache.peek({ identifier, bucket: 'record' })); - } - - disconnectRecord(type: string, id: string | null, lid: string): void; - disconnectRecord(type: string, id: string, lid?: string | null): void; - disconnectRecord(type: StableRecordIdentifier): void; - disconnectRecord(type: string | StableRecordIdentifier, id?: string | null, lid?: string | null): void { - let identifier: StableRecordIdentifier; - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (typeof type === 'string') { - deprecate( - `StoreWrapper.disconnectRecord() has been deprecated in favor of StoreWrapper.disconnectRecord()`, - false, - { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - } - ); - let resource = constructResource(type, id, lid) as RecordIdentifier; - identifier = this.identifierCache.peekRecordIdentifier(resource)!; - } else { - identifier = type; - } - } else { - identifier = type as StableRecordIdentifier; - } - - assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - - this._store._instanceCache.disconnect(identifier); - this._pendingNotifies.delete(identifier); - } -} - -class V2CacheStoreWrapper implements StoreWrapper { - declare _willNotify: boolean; - declare _pendingNotifies: Map>; - declare _store: Store; - - constructor(_store: Store) { - this._store = _store; - this._willNotify = false; - this._pendingNotifies = new Map(); - } - - get identifierCache(): IdentifierCache { - return this._store.identifierCache; - } - - _scheduleNotification(identifier: StableRecordIdentifier, key: string) { - let pending = this._pendingNotifies.get(identifier); - - if (!pending) { - pending = new Set(); - this._pendingNotifies.set(identifier, pending); - } - pending.add(key); - - if (this._willNotify === true) { - return; - } - - this._willNotify = true; - // it's possible a cache adhoc notifies us, - // in which case we sync flush - if (this._store._cbs) { - this._store._schedule('notify', () => this._flushNotifications()); - } else { - // TODO @runspired determine if relationship mutations should schedule - // into join/run vs immediate flush - this._flushNotifications(); - } - } - - _flushNotifications(): void { - if (this._willNotify === false) { - return; - } - - let pending = this._pendingNotifies; - this._pendingNotifies = new Map(); - this._willNotify = false; - - pending.forEach((set, identifier) => { - set.forEach((key) => { - this._store.notifications.notify(identifier, 'relationships', key); - }); - }); - } - - 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 - ): void { - assert(`Expected a stable identifier`, isStableIdentifier(identifier) || isDocumentIdentifier(identifier)); - - // TODO do we still get value from this? - if (namespace === 'relationships' && key) { - this._scheduleNotification(identifier as StableRecordIdentifier, key); - return; - } - - // @ts-expect-error - this._store.notifications.notify(identifier, namespace, key); - } - - getSchemaDefinitionService(): SchemaService { - return this._store.getSchemaDefinitionService(); - } - - recordDataFor(identifier: StableRecordIdentifier): Cache { - if (DEPRECATE_V1_RECORD_DATA) { - deprecate( - `StoreWrapper.recordDataFor is deprecated. With Singleton Cache, this method is no longer needed as the caller is its own cache reference.`, - false, - { - for: '@ember-data/store', - id: 'ember-data:deprecate-record-data-for', - since: { available: '4.10', enabled: '4.10' }, - until: '5.0', - } - ); - } - assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - - return DEPRECATE_V1_RECORD_DATA - ? this._store._instanceCache.getResourceCache(identifier) - : (void 0 as unknown as Cache); - } - - setRecordId(identifier: StableRecordIdentifier, id: string) { - assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - this._store._instanceCache.setRecordId(identifier, id); - } - - hasRecord(identifier: StableRecordIdentifier): boolean { - return Boolean(this._store._instanceCache.peek({ identifier, bucket: 'record' })); - } - - disconnectRecord(identifier: StableRecordIdentifier): void { - assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - this._store._instanceCache.disconnect(identifier); - this._pendingNotifies.delete(identifier); - } -} -export type CacheStoreWrapper = LegacyWrapper | V2CacheStoreWrapper; - -export const CacheStoreWrapper = DEPRECATE_V1CACHE_STORE_APIS ? LegacyWrapper : V2CacheStoreWrapper; diff --git a/packages/store/src/-private/managers/notification-manager.ts b/packages/store/src/-private/managers/notification-manager.ts index dec65e384a3..ddd36534d9a 100644 --- a/packages/store/src/-private/managers/notification-manager.ts +++ b/packages/store/src/-private/managers/notification-manager.ts @@ -1,24 +1,27 @@ /** * @module @ember-data/store */ -import { assert } from '@ember/debug'; + import { _backburner } from '@ember/runloop'; -import { LOG_NOTIFICATIONS } from '@ember-data/debugging'; -import { DEBUG } from '@ember-data/env'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +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 type Store from '../store-service'; +import type { Store } from '../store-service'; export type UnsubscribeToken = object; let tokenId = 0; -const CacheOperations = new Set(['added', 'removed', 'state', 'updated']); +const CacheOperations = new Set(['added', 'removed', 'state', 'updated', 'invalidated']); export type CacheOperation = 'added' | 'removed' | 'updated' | 'state'; +export type DocumentCacheOperation = 'invalidated' | 'added' | 'removed' | 'updated' | 'state'; -function isCacheOperationValue(value: NotificationType | CacheOperation): value is CacheOperation { +function isCacheOperationValue( + value: NotificationType | CacheOperation | DocumentCacheOperation +): value is DocumentCacheOperation { return CacheOperations.has(value); } @@ -32,7 +35,7 @@ export type NotificationType = 'attributes' | 'relationships' | 'identity' | 'er export interface NotificationCallback { (identifier: StableRecordIdentifier, notificationType: 'attributes' | 'relationships', key?: string): void; (identifier: StableRecordIdentifier, notificationType: 'errors' | 'meta' | 'identity' | 'state'): void; - (identifier: StableRecordIdentifier, notificationType: NotificationType, key?: string): void; + // (identifier: StableRecordIdentifier, notificationType: NotificationType, key?: string): void; } export interface ResourceOperationCallback { @@ -42,7 +45,7 @@ export interface ResourceOperationCallback { export interface DocumentOperationCallback { // document updates - (identifier: StableDocumentIdentifier, notificationType: CacheOperation): void; + (identifier: StableDocumentIdentifier, notificationType: DocumentCacheOperation): void; } function _unsubscribe( @@ -53,7 +56,7 @@ function _unsubscribe( Map > ) { - let identifier = tokens.get(token); + const identifier = tokens.get(token); if (LOG_NOTIFICATIONS) { if (!identifier) { // eslint-disable-next-line no-console @@ -123,12 +126,11 @@ export default class NotificationManager { * @public * @param {StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document'} identifier * @param {NotificationCallback | ResourceOperationCallback | DocumentOperationCallback} callback - * @returns {UnsubscribeToken} an opaque token to be used with unsubscribe + * @return {UnsubscribeToken} an opaque token to be used with unsubscribe */ subscribe(identifier: StableRecordIdentifier, callback: NotificationCallback): UnsubscribeToken; subscribe(identifier: 'resource', callback: ResourceOperationCallback): UnsubscribeToken; - subscribe(identifier: StableDocumentIdentifier, callback: DocumentOperationCallback): UnsubscribeToken; - subscribe(identifier: 'document', callback: DocumentOperationCallback): UnsubscribeToken; + subscribe(identifier: 'document' | StableDocumentIdentifier, callback: DocumentOperationCallback): UnsubscribeToken; subscribe( identifier: StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document', callback: NotificationCallback | ResourceOperationCallback | DocumentOperationCallback @@ -147,7 +149,7 @@ export default class NotificationManager { this._cache.set(identifier, map); } - let unsubToken = DEBUG ? { _tokenRef: tokenId++ } : {}; + const unsubToken = DEBUG ? { _tokenRef: tokenId++ } : {}; map.set(unsubToken, callback); this._tokens.set(unsubToken, identifier); return unsubToken; @@ -178,10 +180,11 @@ export default class NotificationManager { */ notify(identifier: StableRecordIdentifier, value: 'attributes' | 'relationships', key?: string): boolean; notify(identifier: StableRecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'state'): boolean; - notify(identifier: StableRecordIdentifier | StableDocumentIdentifier, value: CacheOperation): boolean; + notify(identifier: StableRecordIdentifier, value: CacheOperation): boolean; + notify(identifier: StableDocumentIdentifier, value: DocumentCacheOperation): boolean; notify( identifier: StableRecordIdentifier | StableDocumentIdentifier, - value: NotificationType | CacheOperation, + value: NotificationType | CacheOperation | DocumentCacheOperation, key?: string ): boolean { assert( @@ -216,7 +219,7 @@ export default class NotificationManager { } buffer.push([value, key]); - void this._scheduleNotify(); + this._scheduleNotify(); } return hasSubscribers; @@ -244,14 +247,15 @@ export default class NotificationManager { } _flush() { - if (this._buffered.size) { - this._buffered.forEach((states, identifier) => { + const buffered = this._buffered; + if (buffered.size) { + this._buffered = new Map(); + buffered.forEach((states, identifier) => { states.forEach((args) => { // @ts-expect-error this._flushNotification(identifier, args[0], args[1]); }); }); - this._buffered = new Map(); } this._hasFlush = false; @@ -274,7 +278,7 @@ export default class NotificationManager { // TODO for documents this will need to switch based on Identifier kind if (isCacheOperationValue(value)) { - let callbackMap = this._cache.get(isDocumentIdentifier(identifier) ? 'document' : 'resource') as Map< + const callbackMap = this._cache.get(isDocumentIdentifier(identifier) ? 'document' : 'resource') as Map< UnsubscribeToken, ResourceOperationCallback | DocumentOperationCallback >; @@ -286,7 +290,7 @@ export default class NotificationManager { } } - let callbackMap = this._cache.get(identifier); + const callbackMap = this._cache.get(identifier); if (!callbackMap || !callbackMap.size) { return false; } diff --git a/packages/store/src/-private/managers/record-array-manager.ts b/packages/store/src/-private/managers/record-array-manager.ts index f8a35b30c82..a289dfa0ef0 100644 --- a/packages/store/src/-private/managers/record-array-manager.ts +++ b/packages/store/src/-private/managers/record-array-manager.ts @@ -1,24 +1,25 @@ /** @module @ember-data/store */ -import { ImmutableRequestInfo } from '@ember-data/request/-private/types'; import { addTransactionCB } from '@ember-data/tracking/-private'; -import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { Dict } from '@ember-data/types/q/utils'; - -import IdentifierArray, { +import { getOrSetGlobal } from '@warp-drive/core-types/-private'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; +import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; + +import type { CollectionCreateOptions } from '../record-arrays/identifier-array'; +import { + ARRAY_SIGNAL, Collection, - CollectionCreateOptions, - IDENTIFIER_ARRAY_TAG, + IdentifierArray, NOTIFY, notifyArray, SOURCE, } from '../record-arrays/identifier-array'; -import type Store from '../store-service'; -import { CacheOperation, UnsubscribeToken } from './notification-manager'; +import type { Store } from '../store-service'; +import type { CacheOperation, UnsubscribeToken } from './notification-manager'; -const FAKE_ARR = {}; +const FAKE_ARR = getOrSetGlobal('FAKE_ARR', {}); const SLICE_BATCH_SIZE = 1200; /** * This is a clever optimization. @@ -60,7 +61,7 @@ const SLICE_BATCH_SIZE = 1200; */ export function fastPush(target: T[], source: T[]) { let startLength = 0; - let newLength = source.length; + const newLength = source.length; while (newLength - startLength > SLICE_BATCH_SIZE) { // eslint-disable-next-line prefer-spread target.push.apply(target, source.slice(startLength, startLength + SLICE_BATCH_SIZE)); @@ -76,7 +77,7 @@ type ChangeSet = Map; @class RecordArrayManager @internal */ -class RecordArrayManager { +export class RecordArrayManager { declare store: Store; declare isDestroying: boolean; declare isDestroyed: boolean; @@ -141,8 +142,8 @@ class RecordArrayManager { */ liveArrayFor(type: string): IdentifierArray { let array = this._live.get(type); - let identifiers: StableRecordIdentifier[] = []; - let staged = this._staged.get(type); + const identifiers: StableRecordIdentifier[] = []; + const staged = this._staged.get(type); if (staged) { staged.forEach((value, key) => { if (value === 'add') { @@ -169,11 +170,11 @@ class RecordArrayManager { createArray(config: { type?: string; - query?: ImmutableRequestInfo | Dict; + query?: ImmutableRequestInfo | Record; identifiers?: StableRecordIdentifier[]; doc?: CollectionResourceDocument; }): Collection { - let options: CollectionCreateOptions = { + const options: CollectionCreateOptions = { type: config.type, links: config.doc?.links || null, meta: config.doc?.meta || null, @@ -184,7 +185,7 @@ class RecordArrayManager { store: this.store, manager: this, }; - let array = new Collection(options); + const array = new Collection(options); this._managed.add(array); this._set.set(array, new Set(options.identifiers || [])); if (config.identifiers) { @@ -198,7 +199,7 @@ class RecordArrayManager { if (array === FAKE_ARR) { return; } - let tag = array[IDENTIFIER_ARRAY_TAG]; + const tag = array[ARRAY_SIGNAL]; if (!tag.shouldReset) { tag.shouldReset = true; addTransactionCB(array[NOTIFY]); @@ -216,12 +217,12 @@ class RecordArrayManager { return; } - let liveArray = this._live.get(identifier.type); + const liveArray = this._live.get(identifier.type); const allPending = this._pending; - let pending: Map = new Map(); + const pending: Map = new Map(); if (includeManaged) { - let managed = this._identifiers.get(identifier); + const managed = this._identifiers.get(identifier); if (managed) { managed.forEach((arr) => { let changes = allPending.get(arr); @@ -282,10 +283,10 @@ class RecordArrayManager { } identifierAdded(identifier: StableRecordIdentifier): void { - let changeSets = this._getPendingFor(identifier, false); + const changeSets = this._getPendingFor(identifier, false); if (changeSets) { changeSets.forEach((changes, array) => { - let existing = changes.get(identifier); + const existing = changes.get(identifier); if (existing === 'del') { changes.delete(identifier); } else { @@ -298,10 +299,10 @@ class RecordArrayManager { } identifierRemoved(identifier: StableRecordIdentifier): void { - let changeSets = this._getPendingFor(identifier, true, true); + const changeSets = this._getPendingFor(identifier, true, true); if (changeSets) { changeSets.forEach((changes, array) => { - let existing = changes.get(identifier); + const existing = changes.get(identifier); if (existing === 'add') { changes.delete(identifier); } else { @@ -314,7 +315,7 @@ class RecordArrayManager { } identifierChanged(identifier: StableRecordIdentifier): void { - let newState = this.store._instanceCache.recordIsLoaded(identifier, true); + const newState = this.store._instanceCache.recordIsLoaded(identifier, true); // if the change matches the most recent direct added/removed // state, then we can ignore it @@ -344,7 +345,6 @@ class RecordArrayManager { this.clear(false); this._live.clear(); this.isDestroyed = true; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.store.notifications.unsubscribe(this._subscription); } } @@ -355,7 +355,7 @@ function associate( identifiers: StableRecordIdentifier[] ) { for (let i = 0; i < identifiers.length; i++) { - let identifier = identifiers[i]; + const identifier = identifiers[i]; let cache = ArraysCache.get(identifier); if (!cache) { cache = new Set(); @@ -380,7 +380,7 @@ export function disassociateIdentifier( array: Collection, identifier: StableRecordIdentifier ) { - let cache = ArraysCache.get(identifier); + const cache = ArraysCache.get(identifier); if (cache) { cache.delete(array); } @@ -391,7 +391,7 @@ function sync( changes: Map, arraySet: Set ) { - let state = array[SOURCE]; + const state = array[SOURCE]; const adds: StableRecordIdentifier[] = []; const removes: StableRecordIdentifier[] = []; changes.forEach((value, key) => { @@ -438,5 +438,3 @@ function sync( */ } } - -export default RecordArrayManager; diff --git a/packages/store/src/-private/network/request-cache.ts b/packages/store/src/-private/network/request-cache.ts index fa8bf214516..bb9199ff72a 100644 --- a/packages/store/src/-private/network/request-cache.ts +++ b/packages/store/src/-private/network/request-cache.ts @@ -1,28 +1,58 @@ /** * @module @ember-data/store */ -import { assert } from '@ember/debug'; +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'; -import type { - FindRecordQuery, - Operation, - Request, - RequestState, - SaveRecordMutation, -} from '@ember-data/types/q/fetch-manager'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { FindRecordOptions } from '../../-types/q/store'; +import type { Store } from '../store-service'; -import Store from '../store-service'; +const Touching = getOrSetGlobal('Touching', Symbol('touching')); +export const RequestPromise = getOrSetGlobal('RequestPromise', Symbol('promise')); +const EMPTY_ARR: RequestState[] = DEBUG ? (Object.freeze([]) as unknown as RequestState[]) : []; -const Touching: unique symbol = Symbol('touching'); -export const RequestPromise: unique symbol = Symbol('promise'); +export interface Operation { + op: string; + options: FindRecordOptions | undefined; + recordIdentifier: StableRecordIdentifier; +} + +export interface FindRecordQuery extends Operation { + op: 'findRecord'; +} + +export interface SaveRecordMutation extends Operation { + op: 'saveRecord'; +} + +export interface Request { + data: Operation[]; + options?: Record; +} + +export type RequestStates = 'pending' | 'fulfilled' | 'rejected'; + +export interface RequestState { + state: RequestStates; + type: 'query' | 'mutation'; + request: Request; + response?: Response; +} + +export interface Response { + // rawData: unknown; + data: unknown; +} interface InternalRequest extends RequestState { - [Touching]: RecordIdentifier[]; - [RequestPromise]?: Promise; + [Touching]: StableRecordIdentifier[]; + [RequestPromise]?: Promise; } type RecordOperation = FindRecordQuery | SaveRecordMutation; +export type RequestSubscription = (requestState: RequestState) => void; function hasRecordIdentifier(op: Operation): op is RecordOperation { return 'recordIdentifier' in op; @@ -35,14 +65,14 @@ function hasRecordIdentifier(op: Operation): op is RecordOperation { * @class RequestStateService * @public */ -export default class RequestStateService { - _pending: { [lid: string]: InternalRequest[] } = Object.create(null); +export class RequestStateService { + _pending: Map = new Map(); _done: Map = new Map(); - _subscriptions: { [lid: string]: Function[] } = Object.create(null); + _subscriptions: Map = new Map(); _toFlush: InternalRequest[] = []; _store: Store; - constructor(store) { + constructor(store: Store) { this._store = store; } @@ -51,26 +81,26 @@ export default class RequestStateService { } _enqueue(promise: Promise, queryRequest: Request): Promise { - let query = queryRequest.data[0]; + const query = queryRequest.data[0]; if (hasRecordIdentifier(query)) { - let lid = query.recordIdentifier.lid; - let type = query.op === 'saveRecord' ? ('mutation' as const) : ('query' as const); - if (!this._pending[lid]) { - this._pending[lid] = []; + const identifier = query.recordIdentifier; + const type = query.op === 'saveRecord' ? ('mutation' as const) : ('query' as const); + if (!this._pending.has(identifier)) { + this._pending.set(identifier, []); } - let request: InternalRequest = { + const request: InternalRequest = { state: 'pending', request: queryRequest, type, } as InternalRequest; request[Touching] = [query.recordIdentifier]; request[RequestPromise] = promise; - this._pending[lid].push(request); + this._pending.get(identifier)!.push(request); this._triggerSubscriptions(request); return promise.then( (result) => { - this._dequeue(lid, request); - let finalizedRequest = { + this._dequeue(identifier, request); + const finalizedRequest = { state: 'fulfilled', request: queryRequest, type, @@ -82,8 +112,8 @@ export default class RequestStateService { return result; }, (error) => { - this._dequeue(lid, request); - let finalizedRequest = { + this._dequeue(identifier, request); + const finalizedRequest = { state: 'rejected', request: queryRequest, type, @@ -122,27 +152,32 @@ export default class RequestStateService { _flushRequest(req: InternalRequest): void { req[Touching].forEach((identifier: StableRecordIdentifier) => { - if (this._subscriptions[identifier.lid]) { - this._subscriptions[identifier.lid].forEach((callback) => callback(req)); + const subscriptions = this._subscriptions.get(identifier); + if (subscriptions) { + subscriptions.forEach((callback) => callback(req)); } }); } - _dequeue(lid: string, request: InternalRequest) { - this._pending[lid] = this._pending[lid].filter((req) => req !== request); + _dequeue(identifier: StableRecordIdentifier, request: InternalRequest) { + const pending = this._pending.get(identifier)!; + this._pending.set( + identifier, + pending.filter((req) => req !== request) + ); } _addDone(request: InternalRequest) { request[Touching].forEach((identifier) => { // TODO add support for multiple - let requestDataOp = request.request.data[0].op; + const requestDataOp = request.request.data[0].op; let requests = this._done.get(identifier); if (requests) { requests = requests.filter((req) => { // TODO add support for multiple - let data; - if (req.request.data instanceof Array) { + let data: Operation; + if (Array.isArray(req.request.data)) { data = req.request.data[0]; } else { data = req.request.data; @@ -185,11 +220,13 @@ export default class RequestStateService { * @param {StableRecordIdentifier} identifier * @param {(state: RequestState) => void} callback */ - subscribeForRecord(identifier: RecordIdentifier, callback: (requestState: RequestState) => void) { - if (!this._subscriptions[identifier.lid]) { - this._subscriptions[identifier.lid] = []; + subscribeForRecord(identifier: StableRecordIdentifier, callback: RequestSubscription) { + let subscriptions = this._subscriptions.get(identifier); + if (!subscriptions) { + subscriptions = []; + this._subscriptions.set(identifier, subscriptions); } - this._subscriptions[identifier.lid].push(callback); + subscriptions.push(callback); } /** @@ -198,13 +235,10 @@ export default class RequestStateService { * @method getPendingRequestsForRecord * @public * @param {StableRecordIdentifier} identifier - * @returns {RequestState[]} an array of request states for any pending requests for the given identifier + * @return {RequestState[]} an array of request states for any pending requests for the given identifier */ - getPendingRequestsForRecord(identifier: RecordIdentifier): RequestState[] { - if (this._pending[identifier.lid]) { - return this._pending[identifier.lid]; - } - return []; + getPendingRequestsForRecord(identifier: StableRecordIdentifier): RequestState[] { + return this._pending.get(identifier) || EMPTY_ARR; } /** @@ -213,10 +247,10 @@ export default class RequestStateService { * @method getLastRequestForRecord * @public * @param {StableRecordIdentifier} identifier - * @returns {RequestState | null} the state of the most recent request for the given identifier + * @return {RequestState | null} the state of the most recent request for the given identifier */ - getLastRequestForRecord(identifier: RecordIdentifier): RequestState | null { - let requests = this._done.get(identifier); + getLastRequestForRecord(identifier: StableRecordIdentifier): RequestState | null { + const requests = this._done.get(identifier); if (requests) { return requests[requests.length - 1]; } diff --git a/packages/store/src/-private/proxies/promise-proxies.ts b/packages/store/src/-private/proxies/promise-proxies.ts index d1c16b78189..bc2d0f69b10 100644 --- a/packages/store/src/-private/proxies/promise-proxies.ts +++ b/packages/store/src/-private/proxies/promise-proxies.ts @@ -1,11 +1,9 @@ import { deprecate } from '@ember/debug'; import { get } from '@ember/object'; -import type ComputedProperty from '@ember/object/computed'; -import { reads } from '@ember/object/computed'; -import { DEBUG } from '@ember-data/env'; -import type { Dict } from '@ember-data/types/q/utils'; +import { DEBUG } from '@warp-drive/build-config/env'; +import type IdentifierArray from '../record-arrays/identifier-array'; import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; /** @@ -43,20 +41,6 @@ import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; @extends Ember.ArrayProxy @uses Ember.PromiseProxyMixin */ -interface EmberNativeArrayLike { - length: number | ComputedProperty; - objectAt(idx: number): T | undefined; -} -interface EmberArrayProxyLike { - length: number | ComputedProperty; - objectAtContent(idx: number): T | undefined; -} -type EmberArrayLike = EmberNativeArrayLike | EmberArrayProxyLike; - -export class PromiseArray> extends PromiseArrayProxy { - @reads('content.meta') - declare meta?: Dict; -} /** A `PromiseObject` is an object that acts like both an `EmberObject` @@ -91,17 +75,19 @@ export class PromiseArray> extends PromiseArrayPr */ export { PromiseObjectProxy as PromiseObject }; -function _promiseObject(promise: Promise): PromiseObjectProxy { - return PromiseObjectProxy.create({ promise }) as PromiseObjectProxy; +function _promiseObject(promise: Promise): Promise { + return PromiseObjectProxy.create({ promise }) as Promise; } -function _promiseArray>(promise: Promise): PromiseArray { - return PromiseArray.create({ promise }) as unknown as PromiseArray; +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__']; +const ALLOWABLE_PROPS = ['__ec_yieldable__', '__ec_cancel__'] as const; const PROXIED_ARRAY_PROPS = [ 'length', '[]', @@ -118,17 +104,31 @@ const PROXIED_ARRAY_PROPS = [ ]; const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; -export function promiseArray>(promise: Promise): PromiseArray { - const promiseObjectProxy: PromiseArray = _promiseArray(promise); +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: object, prop: string, receiver: object): unknown { + get(target: SensitiveArray, prop: string, receiver: object): unknown { if (typeof prop === 'symbol') { return Reflect.get(target, prop, receiver); } - if (ALLOWABLE_PROPS.includes(prop)) { + if (isAllowedProp(prop)) { return target[prop]; } if (!ALLOWABLE_METHODS.includes(prop)) { @@ -147,6 +147,7 @@ export function promiseArray>(promise: Promise ); } + // @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); @@ -165,13 +166,13 @@ export function promiseArray>(promise: Promise const ProxySymbolString = String(Symbol.for('PROXY_CONTENT')); -export function promiseObject(promise: Promise): PromiseObjectProxy { - const promiseObjectProxy: PromiseObjectProxy = _promiseObject(promise); +export function promiseObject(promise: Promise): Promise { + const promiseObjectProxy = _promiseObject(promise); if (!DEBUG) { return promiseObjectProxy; } const handler = { - get(target: object, prop: string, receiver: object): unknown { + get(target: SensitiveObject, prop: string, receiver: object): unknown { if (typeof prop === 'symbol') { if (String(prop) === ProxySymbolString) { return; @@ -183,7 +184,7 @@ export function promiseObject(promise: Promise): PromiseObjectProxy { return target.constructor; } - if (ALLOWABLE_PROPS.includes(prop)) { + if (isAllowedProp(prop)) { return target[prop]; } @@ -202,10 +203,12 @@ export function promiseObject(promise: Promise): PromiseObjectProxy { } ); } 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]; } diff --git a/packages/store/src/-private/proxies/promise-proxy-base.d.ts b/packages/store/src/-private/proxies/promise-proxy-base.d.ts index e6b9c1934ef..8c54a04b1de 100644 --- a/packages/store/src/-private/proxies/promise-proxy-base.d.ts +++ b/packages/store/src/-private/proxies/promise-proxy-base.d.ts @@ -2,7 +2,7 @@ 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 Promise {} +export interface PromiseArrayProxy extends ArrayProxy, Promise {} export class PromiseArrayProxy extends ArrayProxy { declare content: T; @@ -10,30 +10,30 @@ export class PromiseArrayProxy extends ArrayProxy { * If the proxied promise is rejected this will contain the reason * provided. */ - reason: string | Error; + declare reason: string | Error; /* * Once the proxied promise has settled this will become `false`. */ - isPending: boolean; + declare isPending: boolean; /* * Once the proxied promise has settled this will become `true`. */ - isSettled: boolean; + declare isSettled: boolean; /* * Will become `true` if the proxied promise is rejected. */ - isRejected: boolean; + declare isRejected: boolean; /* * Will become `true` if the proxied promise is fulfilled. */ - isFulfilled: boolean; + declare isFulfilled: boolean; /* * The promise whose fulfillment value is being proxied by this object. */ - promise: Promise; + declare promise: Promise; } -export interface PromiseObjectProxy extends Promise {} +export interface PromiseObjectProxy extends ObjectProxy, Promise {} export class PromiseObjectProxy extends ObjectProxy { declare content?: T | null; diff --git a/packages/store/src/-private/proxies/promise-proxy-base.js b/packages/store/src/-private/proxies/promise-proxy-base.js index 04c15dc2b55..87c98734d04 100644 --- a/packages/store/src/-private/proxies/promise-proxy-base.js +++ b/packages/store/src/-private/proxies/promise-proxy-base.js @@ -1,7 +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 const PromiseArrayProxy = ArrayProxy.extend(PromiseProxyMixin); - +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 475a08d35ed..ae693334122 100644 --- a/packages/store/src/-private/record-arrays/identifier-array.ts +++ b/packages/store/src/-private/record-arrays/identifier-array.ts @@ -1,38 +1,42 @@ /** @module @ember-data/store */ -// @ts-expect-error -import { tagForProperty } from '@ember/-internals/metal'; -import { assert, deprecate } from '@ember/debug'; +import { deprecate } from '@ember/debug'; import { get, set } from '@ember/object'; -import { dependentKeyCompat } from '@ember/object/compat'; -// eslint-disable-next-line no-restricted-imports import { compare } from '@ember/utils'; -import { tracked } from '@glimmer/tracking'; -// @ts-expect-error -import { dirtyTag } from '@glimmer/validator'; import Ember from 'ember'; +import { compat } from '@ember-data/tracking'; +import type { Signal } from '@ember-data/tracking/-private'; +import { + addToTransaction, + createArrayTags, + createSignal, + defineSignal, + subscribe, +} from '@ember-data/tracking/-private'; import { DEPRECATE_A_USAGE, DEPRECATE_ARRAY_LIKE, DEPRECATE_COMPUTED_CHAINS, DEPRECATE_PROMISE_PROXIES, DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, -} from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; -import { ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import { addToTransaction, subscribe } from '@ember-data/tracking/-private'; -import { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import { Dict } from '@ember-data/types/q/utils'; - +} 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'; +import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record'; +import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; +import type { Links, PaginationLinks } from '@warp-drive/core-types/spec/json-api-raw'; + +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, promiseArray } from '../proxies/promise-proxies'; -import type Store from '../store-service'; +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'; type KeyType = string | symbol | number; const ARRAY_GETTER_METHODS = new Set([ @@ -61,28 +65,24 @@ const ARRAY_GETTER_METHODS = new Set([ ]); const ARRAY_SETTER_METHODS = new Set(['push', 'pop', 'unshift', 'shift', 'splice', 'sort']); const SYNC_PROPS = new Set(['[]', 'length', 'links', 'meta']); -function isArrayGetter(prop: KeyType): boolean { +function isArrayGetter(prop: KeyType): prop is keyof Array { return ARRAY_GETTER_METHODS.has(prop); } -function isArraySetter(prop: KeyType): boolean { +function isArraySetter(prop: KeyType): prop is keyof Array { return ARRAY_SETTER_METHODS.has(prop); } +function isSelfProp(self: T, prop: KeyType): prop is Exclude { + return prop in self; +} -export const IDENTIFIER_ARRAY_TAG = Symbol('#tag'); -export const SOURCE = Symbol('#source'); -export const MUTATE = Symbol('#update'); -export const NOTIFY = Symbol('#notify'); -const IS_COLLECTION = Symbol.for('Collection'); +export const ARRAY_SIGNAL = getOrSetGlobal('#signal', Symbol('#signal')); +export const SOURCE = getOrSetGlobal('#source', Symbol('#source')); +export const MUTATE = getOrSetGlobal('#update', Symbol('#update')); +export const NOTIFY = getOrSetGlobal('#notify', Symbol('#notify')); +const IS_COLLECTION = getOrSetGlobal('IS_COLLECTION', Symbol.for('Collection')); export function notifyArray(arr: IdentifierArray) { - addToTransaction(arr[IDENTIFIER_ARRAY_TAG]); - - if (DEPRECATE_COMPUTED_CHAINS) { - // eslint-disable-next-line - dirtyTag(tagForProperty(arr, 'length')); - // eslint-disable-next-line - dirtyTag(tagForProperty(arr, '[]')); - } + addToTransaction(arr[ARRAY_SIGNAL]); } function convertToInt(prop: KeyType): number | null { @@ -95,43 +95,16 @@ function convertToInt(prop: KeyType): number | null { return num % 1 === 0 ? num : null; } -class Tag { - @tracked ref = null; - declare shouldReset: boolean; - /* - * whether this was part of a transaction when last mutated - */ - declare t: boolean; - declare _debug_base: string; - declare _debug_prop: string; - - constructor() { - if (DEBUG) { - const [arr, prop] = arguments as unknown as [IdentifierArray, string]; - - this._debug_base = arr.constructor.name + ':' + String(arr.modelName); - this._debug_prop = prop; - } - this.shouldReset = false; - this.t = false; - } -} - type ProxiedMethod = (...args: unknown[]) => unknown; -declare global { - interface ProxyConstructor { - new (target: TSource, handler: ProxyHandler): TTarget; - } -} -export type IdentifierArrayCreateOptions = { - identifiers: StableRecordIdentifier[]; - type?: string; +export type IdentifierArrayCreateOptions = { + identifiers: StableRecordIdentifier>[]; + type?: TypeFromInstanceOrString; store: Store; allowMutation: boolean; - manager: RecordArrayManager; + manager: MinimumManager; links?: Links | PaginationLinks | null; - meta?: Dict | null; + meta?: Record | null; }; function deprecateArrayLike(className: string, fnName: string, replName: string) { @@ -149,14 +122,14 @@ function deprecateArrayLike(className: string, fnName: string, replName: string) interface PrivateState { links: Links | PaginationLinks | null; - meta: Dict | null; + meta: Record | null; } -type ForEachCB = (record: RecordInstance, index: number, context: typeof Proxy) => void; -function safeForEach( - instance: typeof Proxy, +type ForEachCB = (record: T, index: number, context: typeof NativeProxy) => void; +function safeForEach( + instance: typeof NativeProxy, arr: StableRecordIdentifier[], store: Store, - callback: ForEachCB, + callback: ForEachCB, target: unknown ) { if (target === undefined) { @@ -173,12 +146,16 @@ function safeForEach( const length = arr.length; // we need to access length to ensure we are consumed for (let index = 0; index < length; index++) { - callback.call(target, store._instanceCache.getRecord(arr[index]), index, instance); + callback.call(target, store._instanceCache.getRecord(arr[index]) as T, index, instance); } return instance; } +type MinimumManager = { + _syncArray: (array: IdentifierArray) => void; +}; + /** A record array is an array that contains records of a certain type (or modelName). The record array materializes records as needed when they are retrieved for the first @@ -191,16 +168,17 @@ function safeForEach( @class RecordArray @public */ -interface IdentifierArray extends Omit, '[]'> { +export interface IdentifierArray extends Omit, '[]'> { [MUTATE]?( target: StableRecordIdentifier[], - receiver: typeof Proxy, + receiver: typeof NativeProxy, prop: string, args: unknown[], - _TAG: Tag + _SIGNAL: Signal ): unknown; } -class IdentifierArray { + +export class IdentifierArray { declare DEPRECATED_CLASS_NAME: string; /** The flag to signal a `RecordArray` is currently loading data. @@ -215,21 +193,22 @@ class IdentifierArray { @public @type Boolean */ - @tracked isUpdating: boolean = false; - isLoaded: boolean = true; - isDestroying: boolean = false; - isDestroyed: boolean = false; - _updatingPromise: PromiseArray | Promise | null = null; + declare isUpdating: boolean; + isLoaded = true; + isDestroying = false; + isDestroyed = false; + _updatingPromise: Promise> | null = null; [IS_COLLECTION] = true; - declare [IDENTIFIER_ARRAY_TAG]: Tag; + declare [ARRAY_SIGNAL]: Signal; [SOURCE]: StableRecordIdentifier[]; [NOTIFY]() { notifyArray(this); } declare links: Links | PaginationLinks | null; - declare meta: Dict | null; + declare meta: Record | null; + declare modelName?: TypeFromInstanceOrString; /** The modelClass represented by this record array. @@ -239,7 +218,7 @@ class IdentifierArray { @deprecated @type {subclass of Model} */ - declare modelName?: string; + declare type: unknown; /** The store that created this record array. @@ -248,7 +227,7 @@ class IdentifierArray { @type Store */ declare store: Store; - declare _manager: RecordArrayManager; + declare _manager: MinimumManager; destroy(clear: boolean) { this.isDestroying = !clear; @@ -260,7 +239,7 @@ class IdentifierArray { } // length must be on self for proxied methods to work properly - @dependentKeyCompat + @compat get length() { return this[SOURCE].length; } @@ -268,56 +247,51 @@ class IdentifierArray { this[SOURCE].length = value; } - // here to support computed chains - // and {{#each}} - get '[]'() { - if (DEPRECATE_COMPUTED_CHAINS) { - return this; - } - } - - constructor(options: IdentifierArrayCreateOptions) { + constructor(options: IdentifierArrayCreateOptions) { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; this.modelName = options.type; this.store = options.store; this._manager = options.manager; this[SOURCE] = options.identifiers; - // @ts-expect-error - this[IDENTIFIER_ARRAY_TAG] = DEBUG ? new Tag(this, 'length') : new Tag(); + this[ARRAY_SIGNAL] = createSignal(this, 'length'); const store = options.store; const boundFns = new Map(); - const _TAG = this[IDENTIFIER_ARRAY_TAG]; + const _SIGNAL = this[ARRAY_SIGNAL]; const PrivateState: PrivateState = { links: options.links || null, meta: options.meta || null, }; - let transaction: boolean = false; + let transaction = false; // when a mutation occurs // we track all mutations within the call // and forward them as one - const proxy = new Proxy(this[SOURCE], { - get(target: StableRecordIdentifier[], prop: KeyType, receiver: typeof Proxy): unknown { - let index = convertToInt(prop); - if (_TAG.shouldReset && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) { + const proxy = new NativeProxy(this[SOURCE], { + get>( + target: StableRecordIdentifier[], + prop: keyof R, + receiver: R + ): unknown { + const index = convertToInt(prop); + if (_SIGNAL.shouldReset && (index !== null || SYNC_PROPS.has(prop) || isArrayGetter(prop))) { options.manager._syncArray(receiver as unknown as IdentifierArray); - _TAG.t = false; - _TAG.shouldReset = false; + _SIGNAL.t = false; + _SIGNAL.shouldReset = false; } if (index !== null) { const identifier = target[index]; if (!transaction) { - subscribe(_TAG); + subscribe(_SIGNAL); } return identifier && store._instanceCache.getRecord(identifier); } - if (prop === 'meta') return subscribe(_TAG), PrivateState.meta; - if (prop === 'links') return subscribe(_TAG), PrivateState.links; - if (prop === '[]') return subscribe(_TAG), receiver; + if (prop === 'meta') return subscribe(_SIGNAL), PrivateState.meta; + if (prop === 'links') return subscribe(_SIGNAL), PrivateState.links; + if (prop === '[]') return subscribe(_SIGNAL), receiver; if (isArrayGetter(prop)) { let fn = boundFns.get(prop); @@ -325,15 +299,15 @@ class IdentifierArray { if (fn === undefined) { if (prop === 'forEach') { fn = function () { - subscribe(_TAG); + subscribe(_SIGNAL); transaction = true; - const result = safeForEach(receiver, target, store, arguments[0] as ForEachCB, arguments[1]); + const result = safeForEach(receiver, target, store, arguments[0] as ForEachCB, arguments[1]); transaction = false; return result; }; } else { fn = function () { - subscribe(_TAG); + subscribe(_SIGNAL); // array functions must run through Reflect to work properly // binding via other means will not work. transaction = true; @@ -364,7 +338,7 @@ class IdentifierArray { 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, _TAG); + const result = self[MUTATE]!(target, receiver, prop as string, args, _SIGNAL); transaction = false; return result; }; @@ -375,29 +349,31 @@ class IdentifierArray { return fn; } - if (prop in self) { + if (isSelfProp(self, prop)) { if (DEPRECATE_ARRAY_LIKE) { if (prop === 'firstObject') { - deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop, '[0]'); + 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, 'at(-1)'); + 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 === IDENTIFIER_ARRAY_TAG || prop === SOURCE) { + if (prop === NOTIFY || prop === ARRAY_SIGNAL || prop === SOURCE) { return self[prop]; } let fn = boundFns.get(prop); if (fn) return fn; - let outcome: unknown = self[prop]; + const outcome: unknown = self[prop]; if (typeof outcome === 'function') { fn = function () { - subscribe(_TAG); + subscribe(_SIGNAL); // array functions must run through Reflect to work properly // binding via other means will not work. return Reflect.apply(outcome as ProxiedMethod, receiver, arguments) as unknown; @@ -407,22 +383,23 @@ class IdentifierArray { return fn; } - return subscribe(_TAG), outcome; + return subscribe(_SIGNAL), outcome; } - return target[prop]; + return target[prop as keyof StableRecordIdentifier[]]; }, + // FIXME: Should this get a generic like get above? set( target: StableRecordIdentifier[], prop: KeyType, value: unknown, - receiver: typeof Proxy + receiver: typeof NativeProxy ): boolean { if (prop === 'length') { if (!transaction && value === 0) { transaction = true; - self[MUTATE]!(target, receiver, 'length 0', [], _TAG); + self[MUTATE]!(target, receiver, 'length 0', [], _SIGNAL); transaction = false; return true; } else if (transaction) { @@ -436,10 +413,10 @@ class IdentifierArray { return true; } if (prop === 'meta') { - PrivateState.meta = (value || null) as Dict | null; + PrivateState.meta = (value || null) as Record | null; return true; } - let index = convertToInt(prop); + const index = convertToInt(prop); // we do not allow "holey" arrays and so if the index is // greater than length then we will disallow setting it. @@ -450,11 +427,12 @@ class IdentifierArray { // a transaction. if (index === null || index > target.length) { if (index !== null && transaction) { - const identifier = recordIdentifierFor(value as RecordInstance); + const identifier = recordIdentifierFor(value); assert(`Cannot set index ${index} past the end of the array.`, isStableIdentifier(identifier)); target[index] = identifier; return true; - } else if (prop in self) { + } else if (isSelfProp(self, prop)) { + // @ts-expect-error not all properties are indeces and we can't safely cast self[prop] = value; return true; } @@ -462,12 +440,14 @@ class IdentifierArray { } if (!options.allowMutation) { - assert(`Mutating ${String(prop)} on this RecordArray is not allowed.`, options.allowMutation); + assert(`Mutating ${String(prop)} on this Array is not allowed.`, options.allowMutation); return false; } - let original: StableRecordIdentifier | undefined = target[index]; - let newIdentifier = extractIdentifierFromRecord(value as RecordInstance); + const original: StableRecordIdentifier | undefined = target[index]; + const newIdentifier = extractIdentifierFromRecord(value); + // FIXME this line was added on main and I'm not sure why + (target as unknown as Record)[index] = newIdentifier; assert(`Expected a record`, isStableIdentifier(newIdentifier)); // We generate "transactions" whenever a setter method on the array // is called and might bulk update multiple array cells. Fundamentally, @@ -486,7 +466,7 @@ class IdentifierArray { // a transaction. // while "arr[arr.length] = newVal;" is handled by this replace cell code path. if (!transaction) { - self[MUTATE]!(target, receiver, 'replace cell', [index, original, newIdentifier], _TAG); + self[MUTATE]!(target, receiver, 'replace cell', [index, original, newIdentifier], _SIGNAL); } else { target[index] = newIdentifier; } @@ -505,11 +485,11 @@ class IdentifierArray { getPrototypeOf() { return IdentifierArray.prototype; }, - }) as IdentifierArray; + }) as IdentifierArray; if (DEPRECATE_A_USAGE) { const meta = Ember.meta(this); - meta.hasMixin = (mixin: Object) => { + meta.hasMixin = (mixin: object) => { deprecate(`Do not call A() on EmberData RecordArrays`, false, { id: 'ember-data:no-a-with-array-like', until: '5.0', @@ -524,11 +504,13 @@ class IdentifierArray { }; } else if (DEBUG) { const meta = Ember.meta(this); - meta.hasMixin = (mixin: Object) => { + meta.hasMixin = (mixin: object) => { assert(`Do not call A() on EmberData RecordArrays`); }; } + createArrayTags(proxy, _SIGNAL); + this[NOTIFY] = this[NOTIFY].bind(proxy); return proxy; @@ -554,15 +536,15 @@ class IdentifierArray { @method update @public */ - update(): PromiseArray | Promise { + update(): Promise> { if (this.isUpdating) { return this._updatingPromise!; } this.isUpdating = true; - let updatingPromise = this._update(); - updatingPromise.finally(() => { + const updatingPromise = this._update(); + void updatingPromise.finally(() => { this._updatingPromise = null; if (this.isDestroying || this.isDestroyed) { return; @@ -579,9 +561,13 @@ class IdentifierArray { Update this RecordArray and return a promise which resolves once the update is finished. */ - _update(): PromiseArray | Promise { + _update(): Promise> { assert(`_update cannot be used with this array`, this.modelName); - return this.store.findAll(this.modelName, { reload: true }); + // @ts-expect-error typescript is unable to handle the complexity of + // T = unknown, modelName = string + // T extends TypedRecordInstance, modelName = TypeFromInstance + // both being valid options to pass through here. + return this.store.findAll(this.modelName, { reload: true }); } // TODO deprecate @@ -600,19 +586,40 @@ class IdentifierArray { @method save @public - @return {PromiseArray} promise + @return {Promise} promise */ - save(): PromiseArray | Promise { - let promise = Promise.all(this.map((record) => this.store.saveRecord(record))).then(() => this); + save(): Promise> { + const promise = Promise.all(this.map((record) => this.store.saveRecord(record))).then(() => this); if (DEPRECATE_PROMISE_PROXIES) { - return promiseArray(promise); + // @ts-expect-error IdentifierArray is not a MutableArray + return promiseArray>(promise); } return promise; } } +// this will error if someone tries to call +// A(identifierArray) since it is not configurable +// which is preferable to the `meta` override we used +// before which required importing all of Ember +const desc = { + enumerable: true, + configurable: false, + get: function () { + // here to support computed chains + // and {{#each}} + if (DEPRECATE_COMPUTED_CHAINS) { + return this; + } + }, +}; +compat(desc); +Object.defineProperty(IdentifierArray.prototype, '[]', desc); + +defineSignal(IdentifierArray.prototype, 'isUpdating', false); + export default IdentifierArray; if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { @@ -639,12 +646,14 @@ if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { } export type CollectionCreateOptions = IdentifierArrayCreateOptions & { - query: ImmutableRequestInfo | Dict | null; + manager: RecordArrayManager; + query: ImmutableRequestInfo | Record | null; isLoaded: boolean; }; -export class Collection extends IdentifierArray { - query: ImmutableRequestInfo | Dict | null = null; +export class Collection extends IdentifierArray { + query: ImmutableRequestInfo | Record | null = null; + declare _manager: RecordArrayManager; constructor(options: CollectionCreateOptions) { super(options as IdentifierArrayCreateOptions); @@ -652,17 +661,23 @@ export class Collection extends IdentifierArray { this.isLoaded = options.isLoaded || false; } - _update(): PromiseArray | Promise { + _update(): Promise> { const { store, query } = this; // TODO save options from initial request? assert(`update cannot be used with this array`, this.modelName); assert(`update cannot be used with no query`, query); - const promise = store.query(this.modelName, query as Dict, { _recordArray: this }); + // @ts-expect-error typescript is unable to handle the complexity of + // T = unknown, modelName = string + // T extends TypedRecordInstance, modelName = TypeFromInstance + // 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; } @@ -693,8 +708,9 @@ if (DEPRECATE_ARRAY_LIKE) { '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.`, @@ -706,23 +722,26 @@ if (DEPRECATE_ARRAY_LIKE) { for: 'ember-data', } ); + // @ts-expect-error ember is missing types for some methods return (Ember[method] as (...args: unknown[]) => unknown)(this, ...args); }; }); - IdentifierArray.prototype.addObject = function (obj: RecordInstance) { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObject = function (obj: OpaqueRecordInstance) { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObject', 'push'); - let index = this.indexOf(obj); + const index = this.indexOf(obj); if (index === -1) { this.push(obj); } return this; }; - IdentifierArray.prototype.addObjects = function (objs: RecordInstance[]) { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObjects = function (objs: OpaqueRecordInstance[]) { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObjects', 'push'); - objs.forEach((obj: RecordInstance) => { - let index = this.indexOf(obj); + objs.forEach((obj: OpaqueRecordInstance) => { + const index = this.indexOf(obj); if (index === -1) { this.push(obj); } @@ -730,65 +749,80 @@ if (DEPRECATE_ARRAY_LIKE) { return this; }; + // @ts-expect-error adding MutableArray method IdentifierArray.prototype.popObject = function () { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'popObject', 'pop'); - return this.pop() as RecordInstance; + return this.pop() as OpaqueRecordInstance; }; - IdentifierArray.prototype.pushObject = function (obj: RecordInstance) { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.pushObject = function (obj: OpaqueRecordInstance) { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObject', 'push'); this.push(obj); return obj; }; - IdentifierArray.prototype.pushObjects = function (objs: RecordInstance[]) { + // @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()!; }; - IdentifierArray.prototype.unshiftObject = function (obj: RecordInstance) { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.unshiftObject = function (obj: OpaqueRecordInstance) { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObject', 'unshift'); this.unshift(obj); return obj; }; - IdentifierArray.prototype.unshiftObjects = function (objs: RecordInstance[]) { + // @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 - let arrIndex = Math.sign(index) === -1 ? this.length + index : index; + 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; }; - IdentifierArray.prototype.insertAt = function (index: number, obj: RecordInstance) { + // @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; }; - IdentifierArray.prototype.removeObject = function (obj: RecordInstance) { + // @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) { @@ -797,7 +831,8 @@ if (DEPRECATE_ARRAY_LIKE) { return this; }; - IdentifierArray.prototype.removeObjects = function (objs: RecordInstance[]) { + // @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); @@ -808,12 +843,15 @@ if (DEPRECATE_ARRAY_LIKE) { 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(); }; - IdentifierArray.prototype.replace = function (idx: number, amt: number, objects?: RecordInstance[]) { + // @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); @@ -822,13 +860,15 @@ if (DEPRECATE_ARRAY_LIKE) { } }; + // @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; }; - IdentifierArray.prototype.setObjects = function (objects: RecordInstance[]) { + // @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`, @@ -839,76 +879,99 @@ if (DEPRECATE_ARRAY_LIKE) { 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'); - let hasValue = arguments.length === 2; + 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'); - let hasValue = arguments.length === 2; + 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++) { - let key = sortKeys[i]; - let propA = get(a, key); - let propB = get(b, key); + 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 - let compareValue = compare(propA, propB); + const compareValue = compare(propA, propB); if (compareValue) { return compareValue; @@ -921,6 +984,7 @@ if (DEPRECATE_ARRAY_LIKE) { // @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)); }; @@ -960,34 +1024,44 @@ if (DEPRECATE_ARRAY_LIKE) { ); }; + // @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(); }; @@ -995,10 +1069,11 @@ if (DEPRECATE_ARRAY_LIKE) { IdentifierArray.prototype.uniqBy = function (key: string) { deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'uniqBy', 'filter'); // all current managed arrays are already enforced as unique - let seen = new Set(); - let result: RecordInstance[] = []; + const seen = new Set(); + const result: OpaqueRecordInstance[] = []; this.forEach((item) => { - let value = get(item, key); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const value = get(item, key); if (seen.has(value)) { return; } @@ -1008,13 +1083,15 @@ if (DEPRECATE_ARRAY_LIKE) { return result; }; - IdentifierArray.prototype.without = function (value: RecordInstance) { + // @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; }; @@ -1024,9 +1101,9 @@ if (DEPRECATE_ARRAY_LIKE) { IdentifierArray.prototype.lastObject = null; } -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) { +function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxyRecord) { assert( `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`, (function () { @@ -1040,13 +1117,13 @@ function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord ); } -function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance | null) { if (!recordOrPromiseRecord) { return null; } if (isPromiseRecord(recordOrPromiseRecord)) { - let content = recordOrPromiseRecord.content; + 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 @@ -1059,6 +1136,6 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | return recordIdentifierFor(recordOrPromiseRecord); } -function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { - return !!record.then; +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); } diff --git a/packages/store/src/-private/record-arrays/native-proxy-type-fix.ts b/packages/store/src/-private/record-arrays/native-proxy-type-fix.ts new file mode 100644 index 00000000000..fc222a39c89 --- /dev/null +++ b/packages/store/src/-private/record-arrays/native-proxy-type-fix.ts @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + We redefine Proxy because the native Proxy type treats the `target` and + `receiver` as the same type incorrectly. + + We ported this from Typescript's own Proxy types on 3/10/2024. +*/ +interface ProxyHandler { + /** + * A trap method for a function call. + * @param target The original callable object which is being proxied. + * @internal + */ + apply?(target: T, thisArg: any, argArray: any[]): any; + + /** + * A trap for the `new` operator. + * @param target The original object which is being proxied. + * @param newTarget The constructor that was originally called. + * @internal + */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + construct?(target: T, argArray: any[], newTarget: Function): object; + + /** + * A trap for `Object.defineProperty()`. + * @param target The original object which is being proxied. + * @returns A `Boolean` indicating whether or not the property has been defined. + * @internal + */ + defineProperty?(target: T, property: string | symbol, attributes: PropertyDescriptor): boolean; + + /** + * A trap for the `delete` operator. + * @param target The original object which is being proxied. + * @param p The name or `Symbol` of the property to delete. + * @returns A `Boolean` indicating whether or not the property was deleted. + * @internal + */ + deleteProperty?(target: T, p: string | symbol): boolean; + + /** + * A trap for getting a property value. + * @param target The original object which is being proxied. + * @param p The name or `Symbol` of the property to get. + * @param receiver The proxy or an object that inherits from the proxy. + * @internal + */ + get?(target: T, p: string | symbol, receiver: any): any; + + /** + * A trap for `Object.getOwnPropertyDescriptor()`. + * @param target The original object which is being proxied. + * @param p The name of the property whose description should be retrieved. + * @internal + */ + getOwnPropertyDescriptor?(target: T, p: string | symbol): PropertyDescriptor | undefined; + + /** + * A trap for the `[[GetPrototypeOf]]` internal method. + * @param target The original object which is being proxied. + * @internal + */ + getPrototypeOf?(target: T): object | null; + + /** + * A trap for the `in` operator. + * @param target The original object which is being proxied. + * @param p The name or `Symbol` of the property to check for existence. + * @internal + */ + has?(target: T, p: string | symbol): boolean; + + /** + * A trap for `Object.isExtensible()`. + * @param target The original object which is being proxied. + * @internal + */ + isExtensible?(target: T): boolean; + + /** + * A trap for `Reflect.ownKeys()`. + * @param target The original object which is being proxied. + * @internal + */ + ownKeys?(target: T): ArrayLike; + + /** + * A trap for `Object.preventExtensions()`. + * @param target The original object which is being proxied. + * @internal + */ + preventExtensions?(target: T): boolean; + + /** + * A trap for setting a property value. + * @param target The original object which is being proxied. + * @param p The name or `Symbol` of the property to set. + * @param receiver The object to which the assignment was originally directed. + * @returns A `Boolean` indicating whether or not the property was set. + * @internal + */ + set?(target: T, p: string | symbol, newValue: any, receiver: any): boolean; + + /** + * A trap for `Object.setPrototypeOf()`. + * @param target The original object which is being proxied. + * @param newPrototype The object's new prototype or `null`. + * @internal + */ + setPrototypeOf?(target: T, v: object | null): boolean; +} + +interface ProxyConstructor { + /** + * Creates a revocable Proxy object. + * @param target A target object to wrap with Proxy. + * @param handler An object whose properties define the behavior of Proxy when an operation is attempted on it. + * @internal + */ + revocable(target: T, handler: ProxyHandler): { proxy: T; revoke: () => void }; + + /** + * Creates a Proxy object. The Proxy object allows you to create an object that can be used in place of the + * original object, but which may redefine fundamental Object operations like getting, setting, and defining + * properties. Proxy objects are commonly used to log property accesses, validate, format, or sanitize inputs. + * @param target A target object to wrap with Proxy. + * @param handler An object whose properties define the behavior of Proxy when an operation is attempted on it. + * @internal + */ + new (target: TSource, handler: ProxyHandler): TTarget; +} + +export const NativeProxy: ProxyConstructor = Proxy as unknown as ProxyConstructor; diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 4a017db55f4..00323a16347 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -1,49 +1,50 @@ /** @module @ember-data/store */ -import { getOwner, setOwner } from '@ember/application'; -import { assert, deprecate } from '@ember/debug'; -import EmberObject from '@ember/object'; -import { _backburner as emberBackburner } from '@ember/runloop'; +// this import location is deprecated but breaks in 4.8 and older +import { deprecate } from '@ember/debug'; -import { importSync } from '@embroider/macros'; +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; -import { LOG_PAYLOADS, LOG_REQUESTS } from '@ember-data/debugging'; +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_JSON_API_FALLBACK, DEPRECATE_PROMISE_PROXIES, + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT, DEPRECATE_STORE_FIND, - DEPRECATE_V1_RECORD_DATA, -} from '@ember-data/deprecations'; -import { DEBUG, TESTING } from '@ember-data/env'; -import type CacheClass from '@ember-data/json-api'; -import type FetchManager from '@ember-data/legacy-compat/legacy-network-handler/fetch-manager'; -import type Model from '@ember-data/model'; -import { HAS_COMPAT_PACKAGE, HAS_GRAPH_PACKAGE, HAS_JSON_API_PACKAGE, HAS_MODEL_PACKAGE } from '@ember-data/packages'; -import type RequestManager from '@ember-data/request'; -import type { Future, ImmutableRequestInfo } from '@ember-data/request/-private/types'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, CacheV1 } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { DSModel } from '@ember-data/types/q/ds-model'; + DISABLE_6X_DEPRECATIONS, + ENABLE_LEGACY_SCHEMA_SERVICE, +} from '@warp-drive/build-config/deprecations'; +import { DEBUG, TESTING } from '@warp-drive/build-config/env'; +import { assert } from '@warp-drive/build-config/macros'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { Graph } from '@warp-drive/core-types/graph'; +import type { + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import { EnableHydration, SkipCache } from '@warp-drive/core-types/request'; +import type { ResourceDocument } from '@warp-drive/core-types/spec/document'; import type { CollectionResourceDocument, EmptyResourceDocument, JsonApiDocument, ResourceIdentifierObject, SingleResourceDocument, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; -import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { SchemaService } from '@ember-data/types/q/schema-service'; -import type { FindOptions } from '@ember-data/types/q/store'; -import type { Dict } from '@ember-data/types/q/utils'; - -import { EnableHydration, type LifetimesService, SkipCache } from './cache-handler'; -import peekCache, { setCacheFor } from './caches/cache-utils'; +} from '@warp-drive/core-types/spec/json-api-raw'; +import type { Type } from '@warp-drive/core-types/symbols'; + +import type { CacheCapabilitiesManager } from '../-types/q/cache-capabilities-manager'; +import type { ModelSchema } from '../-types/q/ds-model'; +import type { OpaqueRecordInstance } from '../-types/q/record-instance'; +import type { SchemaService } from '../-types/q/schema-service'; +import type { FindAllOptions, FindRecordOptions, LegacyResourceQuery, QueryOptions } from '../-types/q/store'; +import type { StoreRequestInput } from './cache-handler/handler'; +import type { CachePolicy } from './cache-handler/types'; import { IdentifierCache } from './caches/identifier-cache'; import { InstanceCache, @@ -51,39 +52,121 @@ import { preloadData, recordIdentifierFor, resourceIsFullyDeleted, - setRecordIdentifier, storeFor, - StoreMap, } from './caches/instance-cache'; -import { Document } from './document'; -import RecordReference from './legacy-model-support/record-reference'; -import { DSModelSchemaDefinitionService, getModelFactory } from './legacy-model-support/schema-definition-service'; -import type ShimModelClass from './legacy-model-support/shim-model-class'; +import type { Document } from './document'; +import type RecordReference from './legacy-model-support/record-reference'; import { getShimClass } from './legacy-model-support/shim-model-class'; -import { legacyCachePut, NonSingletonCacheManager, SingletonCacheManager } from './managers/cache-manager'; +import { CacheManager } from './managers/cache-manager'; import NotificationManager from './managers/notification-manager'; -import RecordArrayManager from './managers/record-array-manager'; -import RequestStateService, { RequestPromise } from './network/request-cache'; -import { PromiseArray, promiseArray, PromiseObject, promiseObject } from './proxies/promise-proxies'; -import IdentifierArray, { Collection } from './record-arrays/identifier-array'; -import coerceId, { ensureStringId } from './utils/coerce-id'; -import constructResource from './utils/construct-resource'; -import normalizeModelName from './utils/normalize-model-name'; +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'; +import { normalizeModelName } from './utils/normalize-model-name'; export { storeFor }; -type StaticModel = typeof Model; - -// hello world -type CacheConstruct = typeof CacheClass; -let _Cache: CacheConstruct | undefined; - -export type HTTPMethod = 'GET' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - -export interface CreateRecordProperties { - id?: string | null; - [key: string]: unknown; -} +// We inline this list of methods to avoid importing EmberObject +type EmberObjectKey = + | '_debugContainerKey' + | '_super' + | 'addObserver' + | 'cacheFor' + | 'concatenatedProperties' + | 'decrementProperty' + | 'destroy' + | 'get' + | 'getProperties' + | 'incrementProperty' + | 'init' + | 'isDestroyed' + | 'isDestroying' + | 'mergedProperties' + | 'notifyPropertyChange' + | 'removeObserver' + | 'reopen' + | 'set' + | 'setProperties' + | 'toggleProperty' + | 'toString' + | 'willDestroy'; + +type DSModelKeys = + | '___(unique) Symbol(Store)' + | '___private_notifications' + | '___recordState' + | '_createSnapshot' + | 'adapterError' + | 'attr' + | 'belongsTo' + | 'changedAttributes' + | 'currentState' + | 'deleteRecord' + | 'destroyRecord' + | 'dirtyType' + | 'eachAttribute' + | 'eachRelationship' + | 'errors' + | 'hasDirtyAttributes' + | 'hasMany' + | 'inverseFor' + | 'isDeleted' + | 'isEmpty' + | 'isError' + | 'isLoaded' + | 'isLoading' + | 'isNew' + | 'isReloading' + | 'isSaving' + | 'isValid' + | 'relationshipFor' + | 'reload' + | 'rollbackAttributes' + | 'save' + | 'serialize' + | 'store' + | 'unloadRecord'; + +type CompatStore = Store & { + adapterFor?: ( + type: string, + _allowMissing?: boolean + ) => undefined | { generateIdForRecord?(store: Store, type: string, properties: object): string }; +}; +function upgradeStore(store: Store): asserts store is CompatStore {} + +type DownlevelArrays = T extends Array ? U[] : T; +type AwaitedKeys = { [K in keyof T & string]: DownlevelArrays> }; + +// `AwaitedKeys` is needed here to resolve any promise types like `PromiseBelongsTo`. +type FilteredKeys = AwaitedKeys>; + +type MaybeHasId = { id?: string | null }; +/** + * Currently only records that extend object can be created via + * store.createRecord. This is a limitation of the current API, + * but can be worked around by creating a new identifier, running + * the cache.clientDidCreate method, and then peeking the record + * for the identifier. + * + * To assign primary key to a record during creation, only `id` will + * work correctly for `store.createRecord`, other primary key may be + * handled by updating the record after creation or using the flow + * described above. + * + * TODO: These are limitations we want to (and can) address. If you + * have need of lifting these limitations, please open an issue. + * + * @typedoc + */ +export type CreateRecordProperties> = T extends TypedRecordInstance + ? Partial> + : T extends MaybeHasId + ? MaybeHasId & Partial> + : MaybeHasId & Record; /** * A Store coordinates interaction between your application, a [Cache](https://api.emberjs.com/ember-data/release/classes/%3CInterface%3E%20Cache), @@ -96,21 +179,260 @@ export interface CreateRecordProperties { * export default class extends Store {} * ``` * - * Most Ember applications will only have a single `Store` configured as a Service + * Most Applications will only have a single `Store` configured as a Service * in this manner. However, setting up multiple stores is possible, including using - * each as a unique service. + * each as a unique service or within a specific context. * @class Store @public */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +const EmptyClass = class { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(args?: unknown) {} +}; +const _BaseClass = macroCondition(dependencySatisfies('ember-source', '*')) + ? DEPRECATE_STORE_EXTENDS_EMBER_OBJECT + ? (importSync('@ember/object') as typeof EmptyClass) + : EmptyClass + : EmptyClass; + +const BaseClass = (_BaseClass as unknown as { default?: typeof EmptyClass }).default + ? ((_BaseClass as unknown as { default?: typeof EmptyClass }).default as typeof EmptyClass) + : _BaseClass; + +if (BaseClass !== EmptyClass) { + deprecate( + `The Store class extending from EmberObject is deprecated. +Please remove usage of EmberObject APIs and mark your class as not requiring it. + +To mark the class as no longer extending from EmberObject, in ember-cli-build.js +set the following config: + +\`\`\`js +const app = new EmberApp(defaults, { + emberData: { + deprecations: { + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: false + } + } +}); +\`\`\` +`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-store-extends-ember-object', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); +} + +export interface Store { + createCache(capabilities: CacheCapabilitiesManager): Cache; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): OpaqueRecordInstance; + + teardownRecord(record: OpaqueRecordInstance): void; + + /* This hook enables an app to supply a SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method will only be called once to instantiate the singleton + * service, which can then be accessed via `store.schema`. + * + * For Example, to use the default SchemaService for SchemaRecord + * + * ```ts + * import { SchemaService } from '@warp-drive/schema-record/schema'; + * + * class extends Store { + * createSchemaService() { + * return new SchemaService(); + * } + * } + * ``` + * + * Or to use the SchemaService for @ember-data/model + * + * ```ts + * import { buildSchema } from '@ember-data/model/hooks'; + * + * class extends Store { + * createSchemaService() { + * return buildSchema(this); + * } + * } + * ``` + * + * If you wish to chain services, you must either + * instantiate each schema source directly or super to retrieve + * an existing service. For convenience, when migrating from + * `@ember-data/model` to `@warp-drive/schema-record` a + * SchemaService is provided that handles this transition + * for you: + * + * ```ts + * import { DelegatingSchemaService } from '@ember-data/model/migration-support'; + * import { SchemaService } from '@warp-drive/schema-record/schema'; + * + * class extends Store { + * createSchemaService() { + * const schema = new SchemaService(); + * return new DelegatingSchemaService(this, schema); + * } + * } + * ``` + * + * When using the DelegateSchemaService, the schema will first + * be sourced from directly registered schemas, then will fallback + * to sourcing a schema from available models if no schema is found. + * + * @method createSchemaService (hook) + * @return {SchemaService} + * @public + */ + createSchemaService(): SchemaService; + + /** + * DEPRECATED - Use the property `store.schema` instead. + * + * Provides access to the SchemaDefinitionService instance + * for this Store instance. + * + * The SchemaDefinitionService can be used to query for + * information about the schema of a resource. + * + * @method getSchemaDefinitionService + * @deprecated + * @public + */ + getSchemaDefinitionService(): SchemaService; + + /** + * DEPRECATED - Use `createSchemaService` instead. + * + * Allows an app to register a custom SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method can only be called more than once, but only one schema + * definition service may exist. Therefore if you wish to chain services + * you must lookup the existing service and close over it with the new + * service by accessing `store.schema` prior to registration. + * + * For Example: + * + * ```ts + * import Store from '@ember-data/store'; + * + * class SchemaDelegator { + * constructor(schema) { + * this._schema = schema; + * } + * + * hasResource(resource: { type: string }): boolean { + * if (AbstractSchemas.has(resource.type)) { + * return true; + * } + * return this._schema.hasResource(resource); + * } + * + * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { + * return this._schema.attributesDefinitionFor(identifier); + * } + * + * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { + * const schema = AbstractSchemas.get(identifier.type); + * return schema || this._schema.relationshipsDefinitionFor(identifier); + * } + * } + * + * export default class extends Store { + * constructor(...args) { + * super(...args); + * + * const schema = this.createSchemaService(); + * this.registerSchemaDefinitionService(new SchemaDelegator(schema)); + * } + * } + * ``` + * + * @method registerSchemaDefinitionService + * @param {SchemaService} schema + * @deprecated + * @public + */ + registerSchemaDefinitionService(schema: SchemaService): void; -// @ts-expect-error -interface Store { - createRecordDataFor?(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper): Cache | CacheV1; + /** + * DEPRECATED - Use `createSchemaService` instead. + * + * Allows an app to register a custom SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method can only be called more than once, but only one schema + * definition service may exist. Therefore if you wish to chain services + * you must lookup the existing service and close over it with the new + * service by accessing `store.schema` prior to registration. + * + * For Example: + * + * ```ts + * import Store from '@ember-data/store'; + * + * class SchemaDelegator { + * constructor(schema) { + * this._schema = schema; + * } + * + * hasResource(resource: { type: string }): boolean { + * if (AbstractSchemas.has(resource.type)) { + * return true; + * } + * return this._schema.hasResource(resource); + * } + * + * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { + * return this._schema.attributesDefinitionFor(identifier); + * } + * + * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { + * const schema = AbstractSchemas.get(identifier.type); + * return schema || this._schema.relationshipsDefinitionFor(identifier); + * } + * } + * + * export default class extends Store { + * constructor(...args) { + * super(...args); + * + * const schema = this.schema; + * this.registerSchema(new SchemaDelegator(schema)); + * } + * } + * ``` + * + * @method registerSchema + * @param {SchemaService} schema + * @deprecated + * @public + */ + registerSchema(schema: SchemaService): void; } -class Store extends EmberObject { +export class Store extends BaseClass { declare recordArrayManager: RecordArrayManager; /** @@ -135,8 +457,11 @@ class Store extends EmberObject { * @property {SchemaService} schema * @public */ - get schema(): SchemaService { - return this.getSchemaDefinitionService(); + get schema(): ReturnType { + if (!this._schema) { + this._schema = this.createSchemaService(); + } + return this._schema as ReturnType; } declare _schema: SchemaService; @@ -163,7 +488,7 @@ class Store extends EmberObject { * ```ts * import Store, { CacheHandler } from '@ember-data/store'; * import RequestManager from '@ember-data/request'; - * import Fetch from '@ember/data/request/fetch'; + * import Fetch from '@ember-data/request/fetch'; * * class extends Store { * constructor() { @@ -181,7 +506,7 @@ class Store extends EmberObject { declare requestManager: RequestManager; /** - * A Property which an App may set to provide a Lifetimes Service + * A Property which an App may set to provide a CachePolicy * to control when a cached request becomes stale. * * Note, when defined, these methods will only be invoked if a @@ -207,21 +532,31 @@ class Store extends EmberObject { * ``` * * @public - * @property {LivetimesService|undefined} lifetimes + * @property {CachePolicy|undefined} lifetimes */ - declare lifetimes?: LifetimesService; + declare lifetimes?: CachePolicy; // Private - declare _adapterCache: Dict; - declare _serializerCache: Dict; - declare _modelFactoryCache: Dict; - declare _fetchManager: FetchManager; + declare _graph?: Graph; declare _requestCache: RequestStateService; declare _instanceCache: InstanceCache; - declare _documentCache: Map>; + declare _documentCache: Map< + StableDocumentIdentifier, + Document + >; declare _cbs: { coalesce?: () => void; sync?: () => void; notify?: () => void } | null; declare _forceShim: boolean; + /** + * Async flush buffers notifications until flushed + * by finalization of a future configured by store.request + * + * This is useful for ensuring that notifications are delivered + * prior to the promise resolving but without risk of promise + * interleaving. + * + * @internal + */ declare _enableAsyncFlush: boolean | null; // DEBUG-only properties @@ -230,14 +565,12 @@ class Store extends EmberObject { declare _isDestroying: boolean; declare _isDestroyed: boolean; - // @ts-expect-error get isDestroying(): boolean { return this._isDestroying; } set isDestroying(value: boolean) { this._isDestroying = value; } - // @ts-expect-error get isDestroyed(): boolean { return this._isDestroyed; } @@ -249,7 +582,7 @@ class Store extends EmberObject { @method init @private */ - constructor(createArgs?: Record) { + constructor(createArgs?: unknown) { super(createArgs); Object.assign(this, createArgs); @@ -263,9 +596,6 @@ class Store extends EmberObject { // private this._requestCache = new RequestStateService(this); this._instanceCache = new InstanceCache(this); - this._adapterCache = Object.create(null); - this._serializerCache = Object.create(null); - this._modelFactoryCache = Object.create(null); this._documentCache = new Map(); this.isDestroying = false; @@ -304,6 +634,16 @@ class Store extends EmberObject { this._cbs = null; } } + + /** + * Executes the callback, ensurng that any work that calls + * store._schedule is executed after in the right order. + * + * When queues already exist, scheduled callbacks will + * join the existing queue. + * + * @internal + */ _join(cb: () => void): void { if (this._cbs) { cb(); @@ -327,7 +667,7 @@ class Store extends EmberObject { * that have been initiated for a given identifier. * * @method getRequestStateService - * @returns {RequestStateService} + * @return {RequestStateService} * @public */ getRequestStateService(): RequestStateService { @@ -336,11 +676,11 @@ class Store extends EmberObject { _getAllPending(): (Promise & { length: number }) | void { if (TESTING) { - const all: Promise[] = []; + const all: Promise[] = []; const pending = this._requestCache._pending; - const lids = Object.keys(pending); - lids.forEach((lid) => { - all.push(...pending[lid].map((v) => v[RequestPromise]!)); + + pending.forEach((requests) => { + all.push(...requests.map((v) => v[RequestPromise]!)); }); this.requestManager._pending.forEach((v) => all.push(v)); const promise: Promise & { length: number } = Promise.allSettled(all) as Promise & { @@ -356,13 +696,14 @@ class Store extends EmberObject { * inserting the response into the cache and handing * back a Future which resolves to a ResponseDocument * - * Resource data is always updated in the cache. + * ## Cache Keys * - * Only GET requests have the request result and document - * cached by default when a cache key is present. + * Only GET requests with a url or requests with an explicit + * cache key (`cacheOptions.key`) will have the request result + * and document cached. * * The cache key used is `requestConfig.cacheOptions.key` - * if present, falling back to `requestconfig.url`. + * if present, falling back to `requestConfig.url`. * * Params are not serialized as part of the cache-key, so * either ensure they are already in the url or utilize @@ -370,20 +711,58 @@ class Store extends EmberObject { * via the `POST` method `requestConfig.cacheOptions.key` * MUST be supplied for the document to be cached. * + * ## Requesting Without a Cache Key + * + * Resource data within the request is always updated in the cache, + * regardless of whether a cache key is present for the request. + * + * ## Fulfilling From Cache + * + * When a cache-key is determined, the request may fulfill + * from cache provided the cache is not stale. + * + * Cache staleness is determined by the configured CachePolicy + * with priority given to the `cacheOptions.reload` and + * `cacheOptions.backgroundReload` on the request if present. + * + * If the cache data has soft expired or the request asks for a background + * reload, the request will fulfill from cache if possible and + * make a non-blocking request in the background to update the cache. + * + * If the cache data has hard expired or the request asks for a reload, + * the request will not fulfill from cache and will make a blocking + * request to update the cache. + * + * ## The Response + * + * The primary difference between `requestManager.request` and `store.request` + * is that `store.request` will attempt to hydrate the response content into + * a response Document containing RecordInstances. + * * @method request - * @param {StoreRequestInfo} requestConfig - * @returns {Future} + * @param {StoreRequestInput} requestConfig + * @return {Future} * @public */ - request(requestConfig: ImmutableRequestInfo): Future { + request(requestConfig: StoreRequestInput): Future { // we lazily set the cache handler when we issue the first request // because constructor doesn't allow for this to run after // the user has had the chance to set the prop. - let opts: { store: Store; disableTestWaiter?: boolean; [EnableHydration]: true } = { + const opts: { + store: Store; + disableTestWaiter?: boolean; + [EnableHydration]: true; + records?: StableRecordIdentifier[]; + } = { store: this, [EnableHydration]: true, }; + if (requestConfig.records) { + const identifierCache = this.identifierCache; + opts.records = requestConfig.records.map((r) => identifierCache.getOrCreateRecordIdentifier(r)); + } + if (TESTING) { if (this.DISABLE_WAITER) { opts.disableTestWaiter = @@ -407,7 +786,8 @@ class Store extends EmberObject { ); } - const future = this.requestManager.request(Object.assign(requestConfig, opts)); + const request = Object.assign({}, requestConfig, opts); + const future = this.requestManager.request(request); future.onFinalize(() => { if (LOG_REQUESTS) { @@ -434,7 +814,7 @@ class Store extends EmberObject { * a resource. * * This hook can be used to select or instantiate any desired - * mechanism of presentating cache data to the ui for access + * mechanism of presenting cache data to the ui for access * mutation, and interaction. * * @method instantiateRecord (hook) @@ -442,197 +822,23 @@ class Store extends EmberObject { * @param createRecordArgs * @param recordDataFor deprecated use this.cache * @param notificationManager deprecated use this.notifications - * @returns A record instance + * @return A record instance * @public */ - instantiateRecord( - identifier: StableRecordIdentifier, - createRecordArgs: { [key: string]: unknown } - ): DSModel | RecordInstance { - if (HAS_MODEL_PACKAGE) { - let modelName = identifier.type; - - const cache = DEPRECATE_V1_RECORD_DATA ? this._instanceCache.getResourceCache(identifier) : this.cache; - // TODO deprecate allowing unknown args setting - let createOptions: any = { - _createProps: createRecordArgs, - // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for - _secretInit: { - identifier, - cache, - store: this, - cb: secretInit, - }, - }; - - // ensure that `getOwner(this)` works inside a model instance - setOwner(createOptions, getOwner(this)!); - const factory = getModelFactory(this, this._modelFactoryCache, modelName); - - assert(`No model was found for '${modelName}'`, factory); - return factory.class.create(createOptions); - } - assert(`You must implement the store's instantiateRecord hook for your custom model class.`); - } /** * A hook which an app or addon may implement. Called when * the Store is destroying a Record Instance. This hook should * be used to teardown any custom record instances instantiated * with `instantiateRecord`. - * - * @method teardownRecord (hook) - * @public - * @param record - */ - teardownRecord(record: DSModel | RecordInstance): void { - if (HAS_MODEL_PACKAGE) { - assert( - `expected to receive an instance of DSModel. If using a custom model make sure you implement teardownRecord`, - 'destroy' in record - ); - (record as DSModel).destroy(); - } else { - assert(`You must implement the store's teardownRecord hook for your custom models`); - } - } - - /** - * Provides access to the SchemaDefinitionService instance - * for this Store instance. - * - * The SchemaDefinitionService can be used to query for - * information about the schema of a resource. - * - * @method getSchemaDefinitionService - * @public - */ - getSchemaDefinitionService(): SchemaService { - if (HAS_MODEL_PACKAGE) { - if (!this._schema) { - // it is potentially a mistake for the RFC to have not enabled chaining these services, though highlander rule is nice. - // what ember-m3 did via private API to allow both worlds to interop would be much much harder using this. - this._schema = new DSModelSchemaDefinitionService(this); - } - } - assert(`You must registerSchemaDefinitionService with the store to use custom model classes`, this._schema); - return this._schema; - } - - /** - * DEPRECATED - Use `registerSchema` instead. - * - * Allows an app to register a custom SchemaService - * for use when information about a resource's schema needs - * to be queried. - * - * This method can only be called more than once, but only one schema - * definition service may exist. Therefore if you wish to chain services - * you must lookup the existing service and close over it with the new - * service by accessing `store.schema` prior to registration. - * - * For Example: - * - * ```ts - * import Store from '@ember-data/store'; - * - * class SchemaDelegator { - * constructor(schema) { - * this._schema = schema; - * } - * - * doesTypeExist(type: string): boolean { - * if (AbstractSchemas.has(type)) { - * return true; - * } - * return this._schema.doesTypeExist(type); - * } - * - * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - * return this._schema.attributesDefinitionFor(identifier); - * } - * - * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - * const schema = AbstractSchemas.get(identifier.type); - * return schema || this._schema.relationshipsDefinitionFor(identifier); - * } - * } - * - * export default class extends Store { - * constructor(...args) { - * super(...args); - * - * const schema = this.schema; - * this.registerSchemaDefinitionService(new SchemaDelegator(schema)); - * } - * } - * ``` - * - * @method registerSchemaDefinitionService - * @param {SchemaService} schema - * @deprecated - * @public - */ - registerSchemaDefinitionService(schema: SchemaService) { - this._schema = schema; - } - /** - * Allows an app to register a custom SchemaService - * for use when information about a resource's schema needs - * to be queried. - * - * This method can only be called more than once, but only one schema - * definition service may exist. Therefore if you wish to chain services - * you must lookup the existing service and close over it with the new - * service by accessing `store.schema` prior to registration. - * - * For Example: - * - * ```ts - * import Store from '@ember-data/store'; - * - * class SchemaDelegator { - * constructor(schema) { - * this._schema = schema; - * } - * - * doesTypeExist(type: string): boolean { - * if (AbstractSchemas.has(type)) { - * return true; - * } - * return this._schema.doesTypeExist(type); - * } - * - * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - * return this._schema.attributesDefinitionFor(identifier); - * } - * - * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - * const schema = AbstractSchemas.get(identifier.type); - * return schema || this._schema.relationshipsDefinitionFor(identifier); - * } - * } - * - * export default class extends Store { - * constructor(...args) { - * super(...args); - * - * const schema = this.schema; - * this.registerSchema(new SchemaDelegator(schema)); - * } - * } - * ``` - * - * @method registerSchema - * @param {SchemaService} schema + * + * @method teardownRecord (hook) * @public + * @param record */ - registerSchema(schema: SchemaService) { - this._schema = schema; - } /** - Returns the schema for a particular `modelName`. + Returns the schema for a particular resource type (modelName). When used with Model from @ember-data/model the return is the model class, but this is not guaranteed. @@ -650,45 +856,22 @@ class Store extends EmberObject { @method modelFor @public - @param {String} modelName - @return {subclass of Model | ShimModelClass} + @deprecated + @param {string} type + @return {ModelSchema} */ - // TODO @deprecate in favor of schema APIs, requires adapter/serializer overhaul or replacement - - modelFor(modelName: string): ShimModelClass | StaticModel { + modelFor(type: TypeFromInstance): ModelSchema; + modelFor(type: string): ModelSchema; + modelFor(type: T extends TypedRecordInstance ? TypeFromInstance : string): ModelSchema { + // FIXME add deprecation and deprecation stripping + // FIXME/TODO update RFC to remove this method if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); } - assert(`You need to pass a model name to the store's modelFor method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - if (HAS_MODEL_PACKAGE) { - let normalizedModelName = normalizeModelName(modelName); - let maybeFactory = getModelFactory(this, this._modelFactoryCache, normalizedModelName); - - // for factorFor factory/class split - const klass = maybeFactory && maybeFactory.class ? maybeFactory.class : null; - - if (!klass || !klass.isModel || this._forceShim) { - assert( - `No model was found for '${modelName}' and no schema handles the type`, - this.getSchemaDefinitionService().doesTypeExist(modelName) - ); - - return getShimClass(this, modelName); - } else { - // TODO @deprecate ever returning the klass, always return the shim - return klass; - } - } + assert(`You need to pass to the store's modelFor method`, typeof type === 'string' && type.length); + assert(`No model was found for '${type}' and no schema handles the type`, this.schema.hasResource({ type })); - assert( - `No model was found for '${modelName}' and no schema handles the type`, - this.getSchemaDefinitionService().doesTypeExist(modelName) - ); - return getShimClass(this, modelName); + return getShimClass(this, type); } /** @@ -706,7 +889,7 @@ class Store extends EmberObject { To create a new instance of a `Post` that has a relationship with a `User` record: ```js - let user = this.store.peekRecord('user', 1); + let user = this.store.peekRecord('user', '1'); store.createRecord('post', { title: 'Ember is awesome!', user: user @@ -715,19 +898,21 @@ class Store extends EmberObject { @method createRecord @public - @param {String} modelName + @param {String} type the name of the resource @param {Object} inputProperties a hash of properties to set on the newly created record. @return {Model} record */ - createRecord(modelName: string, inputProperties: CreateRecordProperties): RecordInstance { + createRecord(type: TypeFromInstance, inputProperties: CreateRecordProperties): T; + createRecord(type: string, inputProperties: CreateRecordProperties): OpaqueRecordInstance; + createRecord(type: string, inputProperties: CreateRecordProperties): OpaqueRecordInstance { if (DEBUG) { assertDestroyingStore(this, 'createRecord'); } - assert(`You need to pass a model name to the store's createRecord method`, modelName); + assert(`You need to pass a model name to the store's createRecord method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); // This is wrapped in a `run.join` so that in test environments users do not need to manually wrap @@ -735,53 +920,48 @@ class Store extends EmberObject { // of record-arrays via ember's run loop, not our own. // // to remove this, we would need to move to a new `async` API. - let record!: RecordInstance; - emberBackburner.join(() => { - this._join(() => { - let normalizedModelName = normalizeModelName(modelName); - let properties = { ...inputProperties }; - - // If the passed properties do not include a primary key, - // give the adapter an opportunity to generate one. Typically, - // client-side ID generators will use something like uuid.js - // to avoid conflicts. - - if (properties.id === null || properties.id === undefined) { - let adapter = this.adapterFor(modelName); - - if (adapter && adapter.generateIdForRecord) { - properties.id = adapter.generateIdForRecord(this, modelName, properties); - } else { - properties.id = null; - } + let record!: OpaqueRecordInstance; + this._join(() => { + const normalizedModelName = normalizeModelName(type); + const properties = { ...inputProperties }; + + // If the passed properties do not include a primary key, + // give the adapter an opportunity to generate one. Typically, + // client-side ID generators will use something like uuid.js + // to avoid conflicts. + let id: string | null = null; + + if (properties.id === null || properties.id === undefined) { + upgradeStore(this); + const adapter = this.adapterFor?.(normalizedModelName, true); + + if (adapter && adapter.generateIdForRecord) { + id = properties.id = coerceId(adapter.generateIdForRecord(this, normalizedModelName, properties)); + } else { + id = properties.id = null; } + } else { + id = properties.id = coerceId(properties.id); + } - // Coerce ID to a string - properties.id = coerceId(properties.id); - const resource = { type: normalizedModelName, id: properties.id }; + const resource = { type: normalizedModelName, id }; - if (resource.id) { - const identifier = this.identifierCache.peekRecordIdentifier(resource as ResourceIdentifierObject); + if (resource.id) { + const identifier = this.identifierCache.peekRecordIdentifier(resource as ResourceIdentifierObject); - assert( - `The id ${properties.id} has already been used with another '${normalizedModelName}' record.`, - !identifier - ); - } + assert( + `The id ${String(properties.id)} has already been used with another '${normalizedModelName}' record.`, + !identifier + ); + } - const identifier = this.identifierCache.createIdentifierForNewRecord(resource); - const cache = DEPRECATE_V1_RECORD_DATA ? this._instanceCache.getResourceCache(identifier) : this.cache; + const identifier = this.identifierCache.createIdentifierForNewRecord(resource); + const cache = this.cache; - const createOptions = normalizeProperties( - this, - identifier, - properties, - (cache as NonSingletonCacheManager).managedVersion === '1' - ); - const resultProps = cache.clientDidCreate(identifier, createOptions); + const createOptions = normalizeProperties(this, identifier, properties); + const resultProps = cache.clientDidCreate(identifier, createOptions); - record = this._instanceCache.getRecord(identifier, resultProps); - }); + record = this._instanceCache.getRecord(identifier, resultProps); }); return record; } @@ -801,25 +981,21 @@ class Store extends EmberObject { @method deleteRecord @public - @param {Model} record + @param {unknown} record */ - deleteRecord(record: RecordInstance): void { + deleteRecord(record: T): void { if (DEBUG) { assertDestroyingStore(this, 'deleteRecord'); } const identifier = peekRecordIdentifier(record); - const cache = - identifier && - (DEPRECATE_V1_RECORD_DATA ? this._instanceCache.peek({ identifier, bucket: 'resourceCache' }) : this.cache); - assert(`expected a cache instance to exist for the record`, cache); + const cache = this.cache; + assert(`expected the record to be connected to a cache`, identifier); this._join(() => { cache.setIsDeleted(identifier, true); if (cache.isNew(identifier)) { - emberBackburner.join(() => { - this._instanceCache.unloadRecord(identifier); - }); + this._instanceCache.unloadRecord(identifier); } }); } @@ -831,7 +1007,7 @@ class Store extends EmberObject { Example ```javascript - store.findRecord('post', 1).then(function(post) { + store.findRecord('post', '1').then(function(post) { store.unloadRecord(post); }); ``` @@ -840,7 +1016,7 @@ class Store extends EmberObject { @public @param {Model} record */ - unloadRecord(record: RecordInstance): void { + unloadRecord(record: T): void { if (DEBUG) { assertDestroyingStore(this, 'unloadRecord'); } @@ -859,7 +1035,7 @@ class Store extends EmberObject { @deprecated @private */ - find(modelName: string, id: string | number, options?): PromiseObject { + find(modelName: string, id: string | number, options?: FindRecordOptions): Promise { if (DEBUG) { assertDestroyingStore(this, 'find'); } @@ -916,8 +1092,6 @@ class Store extends EmberObject { **Example 1** ```app/routes/post.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model({ post_id }) { return this.store.findRecord('post', post_id); @@ -932,8 +1106,6 @@ class Store extends EmberObject { the typical pairing from [JSON:API](https://jsonapi.org/format/#document-resource-object-identification) ```app/routes/post.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model({ post_id: id }) { return this.store.findRecord({ type: 'post', id }); @@ -965,8 +1137,6 @@ class Store extends EmberObject { without also fetching the post you can pass in the post to the `findRecord` call: ```app/routes/post-comments.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model({ post_id, comment_id: id }) { return this.store.findRecord({ type: 'comment', id, { preload: { post: post_id }} }); @@ -978,9 +1148,7 @@ class Store extends EmberObject { snapshot: ```app/adapters/application.js - import EmberObject from '@ember/object'; - - export default class Adapter extends EmberObject { + export default class Adapter { findRecord(store, schema, id, snapshot) { let type = schema.modelName; @@ -992,6 +1160,10 @@ class Store extends EmberObject { .then(response => response.json()) } } + + static create() { + return new this(); + } } ``` @@ -999,8 +1171,6 @@ class Store extends EmberObject { property on the options hash. ```app/routes/post-comments.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model({ post_id, comment_id: id }) { return this.store.findRecord({ type: 'comment', id, { adapterOptions: { post: post_id }} }); @@ -1009,10 +1179,7 @@ class Store extends EmberObject { ``` ```app/adapters/application.js - import EmberObject from '@ember/object'; - - export default class Adapter extends EmberObject { - + export default class Adapter { findRecord(store, schema, id, snapshot) { let type = schema.modelName; @@ -1023,14 +1190,18 @@ class Store extends EmberObject { .then(response => response.json()) } } + + static create() { + return new this(); + } } ``` If you have access to the post model you can also pass the model itself to preload: ```javascript - let post = await store.findRecord('post', 1); - let comment = await store.findRecord('comment', 2, { post: myPostModel }); + let post = await store.findRecord('post', '1'); + let comment = await store.findRecord('comment', '2', { post: myPostModel }); ``` ### Reloading @@ -1059,7 +1230,7 @@ class Store extends EmberObject { // revision: 2 // } // ] - store.findRecord('post', 1, { reload: true }).then(function(post) { + store.findRecord('post', '1', { reload: true }).then(function(post) { post.revision; // 2 }); ``` @@ -1097,7 +1268,7 @@ class Store extends EmberObject { } }); - let blogPost = store.findRecord('post', 1).then(function(post) { + let blogPost = store.findRecord('post', '1').then(function(post) { post.revision; // 1 }); @@ -1118,8 +1289,6 @@ class Store extends EmberObject { `findRecord`. ```app/routes/post/edit.js - import Route from '@ember/routing/route'; - export default class PostEditRoute extends Route { model(params) { return this.store.findRecord('post', params.post_id, { backgroundReload: false }); @@ -1131,8 +1300,6 @@ class Store extends EmberObject { argument it will be passed to your adapter via the snapshot ```app/routes/post/edit.js - import Route from '@ember/routing/route'; - export default class PostEditRoute extends Route { model(params) { return this.store.findRecord('post', params.post_id, { @@ -1172,20 +1339,15 @@ class Store extends EmberObject { comments in the same request: ```app/routes/post.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model(params) { - return this.store.findRecord('post', params.post_id, { include: 'comments' }); + return this.store.findRecord('post', params.post_id, { include: ['comments'] }); } } ``` ```app/adapters/application.js - import EmberObject from '@ember/object'; - - export default class Adapter extends EmberObject { - + export default class Adapter { findRecord(store, schema, id, snapshot) { let type = schema.modelName; @@ -1196,6 +1358,10 @@ class Store extends EmberObject { .then(response => response.json()) } } + + static create() { + return new this(); + } } ``` @@ -1203,16 +1369,14 @@ class Store extends EmberObject { `model.comments`. Multiple relationships can be requested using an `include` parameter consisting of a - comma-separated list (without white-space) while nested relationships can be specified + list of relationship names, while nested relationships can be specified using a dot-separated sequence of relationship names. So to request both the post's comments and the authors of those comments the request would look like this: ```app/routes/post.js - import Route from '@ember/routing/route'; - export default class PostRoute extends Route { model(params) { - return this.store.findRecord('post', params.post_id, { include: 'comments,comments.author' }); + return this.store.findRecord('post', params.post_id, { include: ['comments','comments.author'] }); } } ``` @@ -1243,41 +1407,41 @@ class Store extends EmberObject { Given a `post` model with attributes body, title, publishDate and meta, you can retrieve a filtered list of attributes. ```app/routes/post.js - import Route from '@ember/routing/route'; - export default Route.extend({ + export default class extends Route { model(params) { return this.store.findRecord('post', params.post_id, { adapterOptions: { fields: { post: 'body,title' } }); } - }); + } ``` Moreover, you can filter attributes on related models as well. If a `post` has a `belongsTo` relationship to a user, just include the relationship key and attributes. ```app/routes/post.js - import Route from '@ember/routing/route'; - export default Route.extend({ + export default class extends Route { model(params) { return this.store.findRecord('post', params.post_id, { adapterOptions: { fields: { post: 'body,title', user: 'name,email' } }); } - }); + } ``` @since 1.13.0 @method findRecord @public - @param {String|object} modelName - either a string representing the modelName or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record + @param {String|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record @param {(String|Integer|Object)} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. @return {Promise} promise */ - findRecord(resource: string, id: string | number, options?: FindOptions): PromiseObject; - findRecord(resource: ResourceIdentifierObject, id?: FindOptions): PromiseObject; + findRecord(type: TypeFromInstance, id: string | number, options?: FindRecordOptions): Promise; + findRecord(type: string, id: string | number, options?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject>, options?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject, options?: FindRecordOptions): Promise; findRecord( resource: string | ResourceIdentifierObject, - id?: string | number | FindOptions, - options?: FindOptions - ): PromiseObject { + id?: string | number | FindRecordOptions, + options?: FindRecordOptions + ): Promise { if (DEBUG) { assertDestroyingStore(this, 'findRecord'); } @@ -1287,7 +1451,7 @@ class Store extends EmberObject { resource ); if (isMaybeIdentifier(resource)) { - options = id as FindOptions | undefined; + options = id as FindRecordOptions | undefined; } else { assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${resource}`, @@ -1309,17 +1473,17 @@ class Store extends EmberObject { options.reload = true; } this._join(() => { - preloadData(this, identifier, options!.preload!); + preloadData(this, identifier, options.preload!); }); } - const promise = this.request({ + const promise = this.request({ op: 'findRecord', data: { record: identifier, options, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }); if (DEPRECATE_PROMISE_PROXIES) { @@ -1332,7 +1496,7 @@ class Store extends EmberObject { return promise.then((document) => { return document.content; - }) as PromiseObject; + }); } /** @@ -1341,7 +1505,7 @@ class Store extends EmberObject { Example ```javascript - let userRef = store.getReference('user', 1); + let userRef = store.getReference('user', '1'); // check if the user is loaded let isLoaded = userRef.value() !== null; @@ -1379,7 +1543,7 @@ class Store extends EmberObject { assertDestroyingStore(this, 'getReference'); } - let resourceIdentifier; + let resourceIdentifier: ResourceIdentifierObject; if (arguments.length === 1 && isMaybeIdentifier(resource)) { resourceIdentifier = resource; } else { @@ -1393,7 +1557,7 @@ class Store extends EmberObject { isMaybeIdentifier(resourceIdentifier) ); - let identifier: StableRecordIdentifier = this.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier); + const identifier: StableRecordIdentifier = this.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier); return this._instanceCache.getReference(identifier); } @@ -1412,9 +1576,9 @@ class Store extends EmberObject { **Example 1** ```js - let post = store.peekRecord('post', 1); + let post = store.peekRecord('post', '1'); - post.id; // 1 + post.id; // '1' ``` `peekRecord` can be called with a single identifier argument instead of the combination @@ -1425,7 +1589,7 @@ class Store extends EmberObject { ```js let post = store.peekRecord({ type: 'post', id }); - post.id; // 1 + post.id; // '1' ``` If you have previously received an lid from an Identifier for this record, you can lookup the record again using @@ -1435,7 +1599,7 @@ class Store extends EmberObject { ```js let post = store.peekRecord({ lid }); - post.id; // 1 + post.id; // '1' ``` @@ -1446,15 +1610,17 @@ class Store extends EmberObject { @param {String|Integer} id - optional only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved. @return {Model|null} record */ - peekRecord(identifier: string, id: string | number): RecordInstance | null; - peekRecord(identifier: ResourceIdentifierObject): RecordInstance | null; - peekRecord(identifier: ResourceIdentifierObject | string, id?: string | number): RecordInstance | null { + peekRecord(type: TypeFromInstance, id: string | number): T | null; + peekRecord(type: string, id: string | number): unknown | null; + peekRecord(identifier: ResourceIdentifierObject>): T | null; + peekRecord(identifier: ResourceIdentifierObject): unknown | null; + peekRecord(identifier: ResourceIdentifierObject | string, id?: string | number): T | null { if (arguments.length === 1 && isMaybeIdentifier(identifier)) { const stableIdentifier = this.identifierCache.peekRecordIdentifier(identifier); const isLoaded = stableIdentifier && this._instanceCache.recordIsLoaded(stableIdentifier); // TODO come up with a better mechanism for determining if we have data and could peek. // this is basically an "are we not empty" query. - return isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null; + return isLoaded ? (this._instanceCache.getRecord(stableIdentifier) as T) : null; } if (DEBUG) { @@ -1463,7 +1629,9 @@ class Store extends EmberObject { assert(`You need to pass a model name to the store's peekRecord method`, identifier); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${identifier}`, + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String( + identifier + )}`, typeof identifier === 'string' ); @@ -1473,7 +1641,7 @@ class Store extends EmberObject { const stableIdentifier = this.identifierCache.peekRecordIdentifier(resource); const isLoaded = stableIdentifier && this._instanceCache.recordIsLoaded(stableIdentifier); - return isLoaded ? this._instanceCache.getRecord(stableIdentifier) : null; + return isLoaded ? (this._instanceCache.getRecord(stableIdentifier) as T) : null; } /** @@ -1553,7 +1721,7 @@ class Store extends EmberObject { If you do something like this: ```javascript - store.query('person', { ids: [1, 2, 3] }); + store.query('person', { ids: ['1', '2', '3'] }); ``` The request made to the server will look something like this: @@ -1570,38 +1738,36 @@ class Store extends EmberObject { @since 1.13.0 @method query @public - @param {String} modelName - @param {any} query an opaque query to be used by the adapter + @param {String} type the name of the resource + @param {object} query a query to be used by the adapter @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query @return {Promise} promise */ - query( - modelName: string, - query: Record, - options: { [key: string]: unknown; adapterOptions?: Record } - ): PromiseArray | Promise { + query(type: TypeFromInstance, query: LegacyResourceQuery, options?: QueryOptions): Promise>; + query(type: string, query: LegacyResourceQuery, options?: QueryOptions): Promise; + query(type: string, query: LegacyResourceQuery, options: QueryOptions = {}): Promise { if (DEBUG) { assertDestroyingStore(this, 'query'); } - assert(`You need to pass a model name to the store's query method`, modelName); + assert(`You need to pass a model name to the store's query method`, type); assert(`You need to pass a query hash to the store's query method`, query); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); const promise = this.request({ op: 'query', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), query, - options: options || {}, + options: options, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }); if (DEPRECATE_PROMISE_PROXIES) { - return promiseArray(promise.then((document) => document.content)); + return promiseArray(promise.then((document) => document.content)) as unknown as Promise; } return promise.then((document) => document.content); } @@ -1699,39 +1865,42 @@ class Store extends EmberObject { @since 1.13.0 @method queryRecord @public - @param {String} modelName - @param {any} query an opaque query to be used by the adapter - @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.queryRecord + @param {string} type + @param {object} query an opaque query to be used by the adapter + @param {object} options optional, may include `adapterOptions` hash which will be passed to adapter.queryRecord @return {Promise} promise which resolves with the found record or `null` */ + queryRecord(type: TypeFromInstance, query: LegacyResourceQuery, options?: QueryOptions): Promise; + queryRecord(type: string, query: LegacyResourceQuery, options?: QueryOptions): Promise; queryRecord( - modelName: string, + type: string, query: Record, - options? - ): PromiseObject | Promise { + options?: QueryOptions + ): Promise { if (DEBUG) { assertDestroyingStore(this, 'queryRecord'); } - assert(`You need to pass a model name to the store's queryRecord method`, modelName); + assert(`You need to pass a model name to the store's queryRecord method`, type); assert(`You need to pass a query hash to the store's queryRecord method`, query); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); - const promise = this.request({ + const promise = this.request({ op: 'queryRecord', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), query, options: options || {}, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }); if (DEPRECATE_PROMISE_PROXIES) { return promiseObject(promise.then((document) => document.content)); } + return promise.then((document) => document.content); } @@ -1742,8 +1911,6 @@ class Store extends EmberObject { of them. ```app/routes/authors.js - import Route from '@ember/routing/route'; - export default class AuthorsRoute extends Route { model(params) { return this.store.findAll('author'); @@ -1836,8 +2003,6 @@ class Store extends EmberObject { `findAll`. ```app/routes/post/edit.js - import Route from '@ember/routing/route'; - export default class PostEditRoute extends Route { model() { return this.store.findAll('post', { backgroundReload: false }); @@ -1849,8 +2014,6 @@ class Store extends EmberObject { argument it will be passed to you adapter via the `snapshotRecordArray` ```app/routes/posts.js - import Route from '@ember/routing/route'; - export default class PostsRoute extends Route { model(params) { return this.store.findAll('post', { @@ -1891,25 +2054,21 @@ class Store extends EmberObject { all of the posts' comments in the same request: ```app/routes/posts.js - import Route from '@ember/routing/route'; - export default class PostsRoute extends Route { model() { - return this.store.findAll('post', { include: 'comments' }); + return this.store.findAll('post', { include: ['comments'] }); } } ``` Multiple relationships can be requested using an `include` parameter consisting of a - comma-separated list (without white-space) while nested relationships can be specified + list or relationship names, while nested relationships can be specified using a dot-separated sequence of relationship names. So to request both the posts' comments and the authors of those comments the request would look like this: ```app/routes/posts.js - import Route from '@ember/routing/route'; - export default class PostsRoute extends Route { model() { - return this.store.findAll('post', { include: 'comments,comments.author' }); + return this.store.findAll('post', { include: ['comments','comments.author'] }); } } ``` @@ -1919,36 +2078,36 @@ class Store extends EmberObject { @since 1.13.0 @method findAll @public - @param {String} modelName - @param {Object} options + @param {string} type the name of the resource + @param {object} options @return {Promise} promise */ - findAll( - modelName: string, - options: { reload?: boolean; backgroundReload?: boolean } = {} - ): PromiseArray { + findAll(type: TypeFromInstance, options?: FindAllOptions): Promise>; + findAll(type: string, options?: FindAllOptions): Promise; + findAll(type: TypeFromInstance | string, options: FindAllOptions = {}): Promise> { if (DEBUG) { assertDestroyingStore(this, 'findAll'); } - assert(`You need to pass a model name to the store's findAll method`, modelName); + assert(`You need to pass a model name to the store's findAll method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); - const promise = this.request({ + const promise = this.request>({ op: 'findAll', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), options: options || {}, }, - cacheOptions: { [SkipCache as symbol]: true }, + cacheOptions: { [SkipCache]: true }, }); if (DEPRECATE_PROMISE_PROXIES) { - return promiseArray(promise.then((document) => document.content)); + return promiseArray(promise.then((document) => document.content)) as unknown as Promise>; } - return promise.then((document) => document.content) as PromiseArray; + + return promise.then((document) => document.content); } /** @@ -1973,21 +2132,22 @@ class Store extends EmberObject { @since 1.13.0 @method peekAll @public - @param {String} modelName + @param {string} type the name of the resource @return {RecordArray} */ - peekAll(modelName: string): IdentifierArray { + peekAll(type: TypeFromInstance): IdentifierArray; + peekAll(type: string): IdentifierArray; + peekAll(type: string): IdentifierArray { if (DEBUG) { assertDestroyingStore(this, 'peekAll'); } - assert(`You need to pass a model name to the store's peekAll method`, modelName); + assert(`You need to pass a model name to the store's peekAll method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); - let type = normalizeModelName(modelName); - return this.recordArrayManager.liveArrayFor(type); + return this.recordArrayManager.liveArrayFor(normalizeModelName(type)); } /** @@ -2002,37 +2162,31 @@ class Store extends EmberObject { ``` @method unloadAll + @param {string} type the name of the resource @public - @param {String} modelName */ - unloadAll(modelName?: string) { + unloadAll(type: TypeFromInstance): void; + unloadAll(type?: string): void; + unloadAll(type?: string) { if (DEBUG) { assertDestroyedStoreOnly(this, 'unloadAll'); } assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - !modelName || typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String(type)}`, + !type || typeof type === 'string' ); this._join(() => { - if (modelName === undefined) { + if (type === undefined) { // destroy the graph before unloadAll // since then we avoid churning relationships // during unload - if (HAS_GRAPH_PACKAGE) { - const peekGraph = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .peekGraph; - const graph = peekGraph(this); - if (graph) { - graph.identifiers.clear(); - } - } + this._graph?.identifiers.clear(); this.recordArrayManager.clear(); this._instanceCache.clear(); } else { - let normalizedModelName = normalizeModelName(modelName); - this._instanceCache.clear(normalizedModelName); + this._instanceCache.clear(normalizeModelName(type)); } }); } @@ -2189,17 +2343,18 @@ class Store extends EmberObject { updated. */ push(data: EmptyResourceDocument): null; - push(data: SingleResourceDocument): RecordInstance; - push(data: CollectionResourceDocument): RecordInstance[]; - push(data: JsonApiDocument): RecordInstance | RecordInstance[] | null { + push(data: SingleResourceDocument>): T; + push(data: SingleResourceDocument): OpaqueRecordInstance; + push(data: CollectionResourceDocument>): T[]; + push(data: CollectionResourceDocument): OpaqueRecordInstance[]; + push(data: JsonApiDocument): OpaqueRecordInstance | OpaqueRecordInstance[] | null { if (DEBUG) { assertDestroyingStore(this, 'push'); } - let pushed = this._push(data, false); + const pushed = this._push(data, false); if (Array.isArray(pushed)) { - let records = pushed.map((identifier) => this._instanceCache.getRecord(identifier)); - return records; + return pushed.map((identifier) => this._instanceCache.getRecord(identifier)); } if (pushed === null) { @@ -2216,7 +2371,7 @@ class Store extends EmberObject { @method _push @private @param {Object} jsonApiDoc - @return {StableRecordIdentifier|Array} identifiers for the primary records that had data loaded + @return {StableRecordIdentifier|Array|null} identifiers for the primary records that had data loaded */ _push( jsonApiDoc: JsonApiDocument, @@ -2227,10 +2382,10 @@ class Store extends EmberObject { } if (LOG_PAYLOADS) { try { - let data = JSON.parse(JSON.stringify(jsonApiDoc)); + const data: unknown = JSON.parse(JSON.stringify(jsonApiDoc)) as unknown; // eslint-disable-next-line no-console console.log('EmberData | Payload - push', data); - } catch (e) { + } catch { // eslint-disable-next-line no-console console.log('EmberData | Payload - push', jsonApiDoc); } @@ -2238,160 +2393,49 @@ class Store extends EmberObject { if (asyncFlush) { this._enableAsyncFlush = true; } - let ret; + + let ret!: ResourceDocument; this._join(() => { - if (DEPRECATE_V1_RECORD_DATA) { - ret = legacyCachePut(this, { content: jsonApiDoc }); - } else { - ret = this.cache.put({ content: jsonApiDoc }); - } + ret = this.cache.put({ content: jsonApiDoc }); }); this._enableAsyncFlush = null; - return ret.data; - } - - /** - Push some raw data into the store. - - This method can be used both to push in brand new - records, as well as to update existing records. You - can push in more than one type of object at once. - All objects should be in the format expected by the - serializer. - - ```app/serializers/application.js - import RESTSerializer from '@ember-data/serializer/rest'; - - export default class ApplicationSerializer extends RESTSerializer; - ``` - - ```js - let pushData = { - posts: [ - { id: 1, postTitle: "Great post", commentIds: [2] } - ], - comments: [ - { id: 2, commentBody: "Insightful comment" } - ] - } - - store.pushPayload(pushData); - ``` - - By default, the data will be deserialized using a default - serializer (the application serializer if it exists). - - Alternatively, `pushPayload` will accept a model type which - will determine which serializer will process the payload. - - ```app/serializers/application.js - import RESTSerializer from '@ember-data/serializer/rest'; - - export default class ApplicationSerializer extends RESTSerializer; - ``` - - ```app/serializers/post.js - import JSONSerializer from '@ember-data/serializer/json'; - - export default JSONSerializer; - ``` - - ```js - store.pushPayload(pushData); // Will use the application serializer - store.pushPayload('post', pushData); // Will use the post serializer - ``` - - @method pushPayload - @public - @param {String} modelName Optionally, a model type used to determine which serializer will be used - @param {Object} inputPayload - */ - // TODO @runspired @deprecate pushPayload in favor of looking up the serializer - pushPayload(modelName, inputPayload) { - if (DEBUG) { - assertDestroyingStore(this, 'pushPayload'); - } - let serializer; - let payload; - if (!inputPayload) { - payload = modelName; - serializer = this.serializerFor('application'); - assert( - `You cannot use 'store#pushPayload' without a modelName unless your default serializer defines 'pushPayload'`, - typeof serializer.pushPayload === 'function' - ); - } else { - payload = inputPayload; - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - let normalizedModelName = normalizeModelName(modelName); - serializer = this.serializerFor(normalizedModelName); - } - assert( - `You must define a pushPayload method in your serializer in order to call store.pushPayload`, - serializer.pushPayload - ); - serializer.pushPayload(this, payload); - } - - // TODO @runspired @deprecate records should implement their own serialization if desired - serializeRecord(record: RecordInstance, options?: Dict): unknown { - // TODO we used to check if the record was destroyed here - if (HAS_COMPAT_PACKAGE) { - if (!this._fetchManager) { - const FetchManager = ( - importSync('@ember-data/legacy-compat/-private') as typeof import('@ember-data/legacy-compat/-private') - ).FetchManager; - this._fetchManager = new FetchManager(this); - } - - return this._fetchManager.createSnapshot(recordIdentifierFor(record)).serialize(options); - } - - assert(`Store.serializeRecord is only available when utilizing @ember-data/legacy-compat for legacy compatibility`); + return 'data' in ret ? ret.data : null; } /** * Trigger a save for a Record. * + * Returns a promise resolving with the same record when the save is complete. + * * @method saveRecord * @public - * @param {RecordInstance} record + * @param {unknown} record * @param options - * @returns {Promise} + * @return {Promise} */ - saveRecord(record: RecordInstance, options: Dict = {}): Promise { + saveRecord(record: T, options: Record = {}): Promise { if (DEBUG) { assertDestroyingStore(this, 'saveRecord'); } - assert(`Unable to initate save for a record in a disconnected state`, storeFor(record)); - let identifier = recordIdentifierFor(record); - const cache = - identifier && - (DEPRECATE_V1_RECORD_DATA ? this._instanceCache.peek({ identifier, bucket: 'resourceCache' }) : this.cache); + assert(`Unable to initiate save for a record in a disconnected state`, storeFor(record)); + const identifier = recordIdentifierFor(record); + const cache = this.cache; - if (!cache) { + if (!identifier) { // this commonly means we're disconnected // but just in case we reject here to prevent bad things. - return Promise.reject(`Record Is Disconnected`); + return Promise.reject(new Error(`Record Is Disconnected`)); } - // TODO we used to check if the record was destroyed here assert( - `Cannot initiate a save request for an unloaded record: ${identifier}`, - cache && this._instanceCache.recordIsLoaded(identifier) + `Cannot initiate a save request for an unloaded record: ${identifier.lid}`, + this._instanceCache.recordIsLoaded(identifier) ); if (resourceIsFullyDeleted(this._instanceCache, identifier)) { return Promise.resolve(record); } - if (isDSModel(record)) { - record.errors.clear(); - } - if (!options) { options = {}; } @@ -2409,10 +2453,11 @@ class Store extends EmberObject { options, record: identifier, }, - cacheOptions: { [SkipCache as symbol]: true }, + records: [identifier], + cacheOptions: { [SkipCache]: true }, }; - return this.request(request).then((document) => document.content); + return this.request(request).then((document) => document.content); } /** @@ -2425,19 +2470,8 @@ class Store extends EmberObject { * @method createCache (hook) * @public * @param storeWrapper - * @returns {Cache} + * @return {Cache} */ - createCache(storeWrapper: CacheStoreWrapper): Cache { - if (HAS_JSON_API_PACKAGE) { - if (_Cache === undefined) { - _Cache = (importSync('@ember-data/json-api') as typeof import('@ember-data/json-api')).default; - } - - return new _Cache(storeWrapper); - } - - assert(`Expected store.createCache to be implemented but it wasn't`); - } /** * Returns the cache instance associated to this Store, instantiates the Cache @@ -2446,228 +2480,27 @@ class Store extends EmberObject { * @property {Cache} cache * @public */ - get cache(): Cache { + get cache(): ReturnType { let { cache } = this._instanceCache; if (!cache) { cache = this._instanceCache.cache = this.createCache(this._instanceCache._storeWrapper); if (DEBUG) { - cache = new SingletonCacheManager(cache); - } - } - return cache; - } - - /** - * [DEPRECATED] use Store.createCache - * - * Instantiation hook allowing applications or addons to configure the store - * to utilize a custom RecordData implementation. - * - * @method createRecordDataFor (hook) - * @deprecated - * @public - * @param identifier - * @param storeWrapper - * @returns {Cache} - */ - - /** - `normalize` converts a json payload into the normalized form that - [push](../methods/push?anchor=push) expects. - - Example - - ```js - socket.on('message', function(message) { - let modelName = message.model; - let data = message.data; - store.push(store.normalize(modelName, data)); - }); - ``` - - @method normalize - @public - @param {String} modelName The name of the model type for this payload - @param {Object} payload - @return {Object} The normalized payload - */ - // TODO @runspired @deprecate users should call normalize on the associated serializer directly - normalize(modelName: string, payload) { - if (DEBUG) { - assertDestroyingStore(this, 'normalize'); - } - assert(`You need to pass a model name to the store's normalize method`, modelName); - assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, - typeof modelName === 'string' - ); - let normalizedModelName = normalizeModelName(modelName); - let serializer = this.serializerFor(normalizedModelName); - let model = this.modelFor(normalizedModelName); - assert( - `You must define a normalize method in your serializer in order to call store.normalize`, - serializer?.normalize - ); - return serializer.normalize(model, payload); - } - - /** - Returns an instance of the adapter for a given type. For - example, `adapterFor('person')` will return an instance of - the adapter located at `app/adapters/person.js` - - If no `person` adapter is found, this method will look - for an `application` adapter (the default adapter for - your entire application). - - @method adapterFor - @public - @param {String} modelName - @return Adapter - */ - adapterFor(modelName: string) { - if (DEBUG) { - assertDestroyingStore(this, 'adapterFor'); - } - assert(`You need to pass a model name to the store's adapterFor method`, modelName); - assert( - `Passing classes to store.adapterFor has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - let normalizedModelName = normalizeModelName(modelName); - - let { _adapterCache } = this; - let adapter = _adapterCache[normalizedModelName]; - if (adapter) { - return adapter; - } - - let owner: any = getOwner(this); - - // name specific adapter - adapter = owner.lookup(`adapter:${normalizedModelName}`); - if (adapter !== undefined) { - _adapterCache[normalizedModelName] = adapter; - return adapter; - } - - // no adapter found for the specific name, fallback and check for application adapter - adapter = _adapterCache.application || owner.lookup('adapter:application'); - if (adapter !== undefined) { - _adapterCache[normalizedModelName] = adapter; - _adapterCache.application = adapter; - 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; + cache = new CacheManager(cache); } } - assert(`No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`); - } - - /** - Returns an instance of the serializer for a given type. For - example, `serializerFor('person')` will return an instance of - `App.PersonSerializer`. - - If no `App.PersonSerializer` is found, this method will look - for an `App.ApplicationSerializer` (the default serializer for - your entire application). - - If a serializer cannot be found on the adapter, it will fall back - to an instance of `JSONSerializer`. - - @method serializerFor - @public - @param {String} modelName the record to serialize - @return {Serializer} - */ - serializerFor(modelName: string): MinimumSerializerInterface | null { - if (DEBUG) { - assertDestroyingStore(this, 'serializerFor'); - } - assert(`You need to pass a model name to the store's serializerFor method`, modelName); - assert( - `Passing classes to store.serializerFor has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' - ); - let normalizedModelName = normalizeModelName(modelName); - - let { _serializerCache } = this; - let serializer = _serializerCache[normalizedModelName]; - if (serializer) { - return serializer; - } - - let owner: any = getOwner(this); - - // by name - serializer = owner.lookup(`serializer:${normalizedModelName}`); - if (serializer !== undefined) { - _serializerCache[normalizedModelName] = serializer; - return serializer; - } - - // no serializer found for the specific model, fallback and check for application serializer - serializer = _serializerCache.application || owner.lookup('serializer:application'); - if (serializer !== undefined) { - _serializerCache[normalizedModelName] = serializer; - _serializerCache.application = serializer; - return serializer; - } - - return null; + return cache as ReturnType; } - // @ts-expect-error destroy(): void { if (this.isDestroyed) { // @ember/test-helpers will call destroy multiple times return; } this.isDestroying = true; - // enqueue destruction of any adapters/serializers we have created - for (let adapterName in this._adapterCache) { - let adapter = this._adapterCache[adapterName]!; - if (typeof adapter.destroy === 'function') { - adapter.destroy(); - } - } - - for (let serializerName in this._serializerCache) { - let serializer = this._serializerCache[serializerName]!; - if (typeof serializer.destroy === 'function') { - serializer.destroy(); - } - } - if (HAS_GRAPH_PACKAGE) { - const peekGraph = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .peekGraph; - let graph = peekGraph(this); - if (graph) { - graph.destroy(); - } - } + this._graph?.destroy(); + this._graph = undefined; this.notifications.destroy(); this.recordArrayManager.destroy(); @@ -2682,19 +2515,71 @@ class Store extends EmberObject { } } -export default Store; +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()\``, + /* 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()\``, + /* 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()\``, + /* 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; + }; +} -let assertDestroyingStore: Function; -let assertDestroyedStoreOnly: Function; +let assertDestroyingStore: (store: Store, method: string) => void; +let assertDestroyedStoreOnly: (store: Store, method: string) => void; if (DEBUG) { - assertDestroyingStore = function assertDestroyedStore(store, method) { + // eslint-disable-next-line @typescript-eslint/no-shadow + assertDestroyingStore = function assertDestroyingStore(store: Store, method: string) { assert( `Attempted to call store.${method}(), but the store instance has already been destroyed.`, !(store.isDestroying || store.isDestroyed) ); }; - assertDestroyedStoreOnly = function assertDestroyedStoreOnly(store, method) { + // eslint-disable-next-line @typescript-eslint/no-shadow + assertDestroyedStoreOnly = function assertDestroyedStoreOnly(store: Store, method: string) { assert( `Attempted to call store.${method}(), but the store instance has already been destroyed.`, !store.isDestroyed @@ -2713,18 +2598,10 @@ function isMaybeIdentifier( ); } -function isDSModel(record: RecordInstance | null): record is DSModel { - if (!HAS_MODEL_PACKAGE) { - return false; - } - return !!record && 'constructor' in record && 'isModel' in record.constructor && record.constructor.isModel === true; -} - function normalizeProperties( store: Store, identifier: StableRecordIdentifier, - properties?: { [key: string]: unknown }, - isForV1: boolean = false + properties?: { [key: string]: unknown } ): { [key: string]: unknown } | undefined { // assert here if (properties !== undefined) { @@ -2739,27 +2616,24 @@ function normalizeProperties( const { type } = identifier; // convert relationship Records to RecordDatas before passing to RecordData - let defs = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); + const defs = store.schema.fields({ type }); - if (defs !== null) { - let keys = Object.keys(properties); - let relationshipValue; + if (defs.size) { + const keys = Object.keys(properties); for (let i = 0; i < keys.length; i++) { - let prop = keys[i]; - let def = defs[prop]; + const prop = keys[i]; + const field = defs.get(prop); - if (def !== undefined) { - if (def.kind === 'hasMany') { - if (DEBUG) { - assertRecordsPassedToHasMany(properties[prop] as RecordInstance[]); - } - relationshipValue = extractIdentifiersFromRecords(properties[prop] as RecordInstance[], isForV1); - } else { - relationshipValue = extractIdentifierFromRecord(properties[prop] as RecordInstance, isForV1); - } + if (!field) continue; - properties[prop] = relationshipValue; + if (field.kind === 'hasMany') { + if (DEBUG) { + assertRecordsPassedToHasMany(properties[prop] as OpaqueRecordInstance[]); + } + properties[prop] = extractIdentifiersFromRecords(properties[prop] as OpaqueRecordInstance[]); + } else if (field.kind === 'belongsTo') { + properties[prop] = extractIdentifierFromRecord(properties[prop]); } } } @@ -2767,7 +2641,7 @@ function normalizeProperties( return properties; } -function assertRecordsPassedToHasMany(records: RecordInstance[]) { +function assertRecordsPassedToHasMany(records: OpaqueRecordInstance[]) { assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); assert( `All elements of a hasMany relationship must be instances of Model, you passed ${records @@ -2786,24 +2660,21 @@ function assertRecordsPassedToHasMany(records: RecordInstance[]) { ); } -function extractIdentifiersFromRecords(records: RecordInstance[], isForV1: boolean = false): StableRecordIdentifier[] { - return records.map((record) => extractIdentifierFromRecord(record, isForV1)) as StableRecordIdentifier[]; +function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableRecordIdentifier[] { + return records.map((record) => extractIdentifierFromRecord(record)) as StableRecordIdentifier[]; } -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function extractIdentifierFromRecord( - recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null, - isForV1: boolean = false -) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance | null) { if (!recordOrPromiseRecord) { return null; } - const extract = isForV1 ? peekCache : recordIdentifierFor; + const extract = recordIdentifierFor; if (DEPRECATE_PROMISE_PROXIES) { if (isPromiseRecord(recordOrPromiseRecord)) { - let content = recordOrPromiseRecord.content; + 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 @@ -2828,12 +2699,6 @@ function extractIdentifierFromRecord( return extract(recordOrPromiseRecord); } -function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { - return !!record.then; -} - -function secretInit(record: RecordInstance, cache: Cache, identifier: StableRecordIdentifier, store: Store): void { - setRecordIdentifier(record, identifier); - StoreMap.set(record, store); - setCacheFor(record, cache); +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 new file mode 100644 index 00000000000..5ee28e360c2 --- /dev/null +++ b/packages/store/src/-private/store-service.type-test.ts @@ -0,0 +1,420 @@ +import { expectTypeOf } from 'expect-type'; + +import { Type } from '@warp-drive/core-types/symbols'; + +import type { Collection, IdentifierArray } from './record-arrays/identifier-array'; +import type { CreateRecordProperties } from './store-service'; +import { Store } from './store-service'; + +////////////////////////////////// +////////////////////////////////// +// store.peekRecord +////////////////////////////////// +////////////////////////////////// +{ + const store = new Store(); + + type UnbrandedUser = { + name: string; + }; + type BrandedUser = { + name: string; + [Type]: 'user'; + }; + + const result1 = store.peekRecord('user', '1'); + + expectTypeOf(result1).toBeUnknown(); + expectTypeOf( + store.peekRecord( + // @ts-expect-error since there is no brand, this should error + 'user', + '1' + ) + ).toEqualTypeOf(); + + expectTypeOf(store.peekRecord('user', '1')).toEqualTypeOf(); + expectTypeOf( + store.peekRecord( + // @ts-expect-error should error since this does not match the brand + 'users', + '1' + ) + ).toEqualTypeOf(); +} + +////////////////////////////////// +////////////////////////////////// +// store.findRecord +////////////////////////////////// +////////////////////////////////// +{ + const store = new Store(); + + type UnbrandedUser = { + name: string; + }; + type BrandedUser = { + name: string; + [Type]: 'user'; + }; + + expectTypeOf(store.findRecord('user', '1')).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error no matching signature since no brand from which to check 'user' + store.findRecord('user', '1') + ).toEqualTypeOf>(); + expectTypeOf(store.findRecord('user', '1')).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error should error since this does not match the brand + store.findRecord('users', '1') + ).toEqualTypeOf>(); + + type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; + }; + type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; + }; + type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; + }; + + const result = await store.findRecord('thing', '1'); + const result2 = await store.findRecord('thing', '1', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }); + + expectTypeOf(result); + expectTypeOf(result2); +} + +////////////////////////////////// +////////////////////////////////// +// store.queryRecord +////////////////////////////////// +////////////////////////////////// +{ + const store = new Store(); + + type UnbrandedUser = { + name: string; + }; + type BrandedUser = { + name: string; + [Type]: 'user'; + }; + + // @ts-expect-error expect error since no second argument + void store.queryRecord('user'); + + expectTypeOf(store.queryRecord('user', {})).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error no matching signature since no brand from which to check 'user' + store.queryRecord('user', {}) + ).toEqualTypeOf>(); + expectTypeOf(store.queryRecord('user', {})).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error should error since this does not match the brand + store.queryRecord('users', {}) + ).toEqualTypeOf>(); + + type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; + }; + type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; + }; + type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; + }; + + const result = await store.queryRecord('thing', {}); + const result2 = await store.queryRecord('thing', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }); + const result3 = await store.queryRecord('thing', { + // expect no error because we did not pass a generic + include: [ + 'name', + 'relatedThing', + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + 'relatedThing.relatedThing', + ], + }); + + expectTypeOf(result); + expectTypeOf(result3); + expectTypeOf(result2); +} + +////////////////////////////////// +////////////////////////////////// +// store.findAll +////////////////////////////////// +////////////////////////////////// +{ + const store = new Store(); + + type UnbrandedUser = { + name: string; + }; + type BrandedUser = { + name: string; + [Type]: 'user'; + }; + + expectTypeOf(store.findAll('user')).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error no matching signature since no brand from which to check 'user' + store.findAll('user') + ).toEqualTypeOf>>(); + expectTypeOf(store.findAll('user')).toEqualTypeOf>>(); + expectTypeOf( + // @ts-expect-error should error since this does not match the brand + store.findAll('users') + ).toEqualTypeOf>>(); + + type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; + }; + type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; + }; + type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; + }; + + const result = await store.findAll('thing'); + const result2 = await store.findAll('thing', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }); + + expectTypeOf(result); + expectTypeOf>(result2); +} + +////////////////////////////////// +////////////////////////////////// +// store.query +////////////////////////////////// +////////////////////////////////// +{ + const store = new Store(); + + type UnbrandedUser = { + name: string; + }; + type BrandedUser = { + name: string; + [Type]: 'user'; + }; + + // @ts-expect-error expect error since no second argument + void store.query('user'); + + expectTypeOf(store.query('user', {})).toEqualTypeOf>(); + expectTypeOf( + // @ts-expect-error no matching signature since no brand from which to check 'user' + store.query('user', {}) + ).toEqualTypeOf>>(); + expectTypeOf(store.query('user', {})).toEqualTypeOf>>(); + expectTypeOf( + // @ts-expect-error should error since this does not match the brand + store.query('users', {}) + ).toEqualTypeOf>>(); + + type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [Type]: 'thing'; + }; + type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [Type]: 'other-thing'; + }; + type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [Type]: 'deep-thing'; + }; + + const result = await store.query('thing', {}); + const result2 = await store.query('thing', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }); + + const result3 = await store.query('thing', { + // we expect no errors here since we did not pass a generic to query + include: [ + 'name', + 'relatedThing', + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + 'relatedThing.relatedThing', + ], + }); + + expectTypeOf(result); + expectTypeOf(result3); + expectTypeOf>(result2); +} + +////////////////////////////////// +////////////////////////////////// +// type CreateRecordProperties +////////////////////////////////// +////////////////////////////////// +{ + class MockModel { + [Type] = 'user' as const; + asyncProp = Promise.resolve('async'); + syncProp = 'sync'; + + // some fake Model properties + + // some fake EmberObject properties + reopen(): void {} + destroy(): void {} + init(): void {} + isDestroyed = false; + isDestroying = false; + willDestroy(): void {} + } + + const mock = new MockModel(); + + expectTypeOf(mock.asyncProp).toEqualTypeOf>(); + expectTypeOf(mock.syncProp).toEqualTypeOf(); + + const result: CreateRecordProperties = {}; + + // Only `asyncProp` and `syncProp` should be present in the type, they should be optional and + // any Promise types should be awaited. + expectTypeOf(result).toEqualTypeOf<{ + asyncProp?: string; + syncProp?: string; + }>(); + + const fullResult: Required> = { + asyncProp: 'async', + syncProp: 'sync', + }; + + expectTypeOf(fullResult).toEqualTypeOf<{ + asyncProp: string; + syncProp: string; + }>(); +} diff --git a/packages/store/src/-private/utils/coerce-id.ts b/packages/store/src/-private/utils/coerce-id.ts index 6aa9961536e..471b1983d0a 100644 --- a/packages/store/src/-private/utils/coerce-id.ts +++ b/packages/store/src/-private/utils/coerce-id.ts @@ -2,6 +2,11 @@ @module @ember-data/store */ +import { deprecate } from '@ember/debug'; + +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 // that developers may provide IDs as numbers (e.g., `store.findRecord('person', 1)`), // it is important that internally we use strings, since IDs may be serialized @@ -10,19 +15,41 @@ // corresponding record, we will not know if it is a string or a number. type Coercable = string | number | boolean | null | undefined | symbol; -function coerceId(id: Coercable): string | null { - if (id === null || id === undefined || id === '') { - return null; - } - if (typeof id === 'string') { - return id; - } - if (typeof id === 'symbol') { - return id.toString(); +export function coerceId(id: unknown): string | null { + if (DEPRECATE_NON_STRICT_ID) { + let normalized: string | null; + if (id === null || id === undefined || id === '') { + normalized = null; + } else { + normalized = String(id); + } + + deprecate( + `The resource id '<${typeof id}> ${String( + id + )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, + { + id: 'ember-data:deprecate-non-strict-id', + until: '6.0', + for: 'ember-data', + since: { + available: '5.3', + enabled: '5.3', + }, + } + ); + + return normalized; } - return '' + id; -} + assert( + `Resource IDs must be a non-empty string or null. Received '${String(id)}'.`, + id === null || (typeof id === 'string' && id.length > 0) + ); + + return id; +} export function ensureStringId(id: Coercable): string { let normalized: string | null = null; if (typeof id === 'string') { diff --git a/packages/store/src/-private/utils/construct-resource.ts b/packages/store/src/-private/utils/construct-resource.ts index 12710992cea..8902b1b93af 100644 --- a/packages/store/src/-private/utils/construct-resource.ts +++ b/packages/store/src/-private/utils/construct-resource.ts @@ -1,30 +1,33 @@ -import { assert } from '@ember/debug'; - +import { assert } from '@warp-drive/build-config/macros'; import type { ExistingResourceIdentifierObject, ResourceIdentifierObject, -} from '@ember-data/types/q/ember-data-json-api'; +} from '@warp-drive/core-types/spec/json-api-raw'; import { isStableIdentifier } from '../caches/identifier-cache'; -import coerceId from './coerce-id'; -import isNonEmptyString from './is-non-empty-string'; +import { coerceId } from './coerce-id'; +import { isNonEmptyString } from './is-non-empty-string'; -function constructResource(type: ResourceIdentifierObject): ResourceIdentifierObject; -function constructResource(type: string, id: string, lid: string): ExistingResourceIdentifierObject; -function constructResource( +export function constructResource(type: ResourceIdentifierObject): ResourceIdentifierObject; +export function constructResource(type: string, id: string, lid: string): ExistingResourceIdentifierObject; +export function constructResource( type: string | undefined, id: null | undefined, lid: string ): ExistingResourceIdentifierObject; -function constructResource(type: string, id: string, lid?: string | null): ExistingResourceIdentifierObject; -function constructResource(type: string, id?: string | number | null, lid?: string | null): ResourceIdentifierObject; -function constructResource( +export function constructResource(type: string, id: string, lid?: string | null): ExistingResourceIdentifierObject; +export function constructResource( + type: string, + id?: string | number | null, + lid?: string | null +): ResourceIdentifierObject; +export function constructResource( type: string | ResourceIdentifierObject | undefined, id?: string | number | null, lid?: string | null ): ResourceIdentifierObject | ExistingResourceIdentifierObject { if (typeof type === 'object' && type !== null) { - let resource = type; + const resource = type; if (isStableIdentifier(resource)) { return resource; } @@ -57,5 +60,3 @@ function constructResource( return { type, id: trueId }; } } - -export default constructResource; diff --git a/packages/store/src/-private/utils/identifier-debug-consts.ts b/packages/store/src/-private/utils/identifier-debug-consts.ts deleted file mode 100644 index fd5842acf90..00000000000 --- a/packages/store/src/-private/utils/identifier-debug-consts.ts +++ /dev/null @@ -1,3 +0,0 @@ -// 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'); diff --git a/packages/store/src/-private/utils/is-non-empty-string.ts b/packages/store/src/-private/utils/is-non-empty-string.ts index 1714a19e48b..12f74b3e3cc 100644 --- a/packages/store/src/-private/utils/is-non-empty-string.ts +++ b/packages/store/src/-private/utils/is-non-empty-string.ts @@ -1,3 +1,3 @@ -export default function isNonEmptyString(str: any): str is string { - return str && typeof str === 'string'; +export function isNonEmptyString(str: unknown): str is string { + return Boolean(str && typeof str === 'string'); } diff --git a/packages/store/src/-private/utils/normalize-model-name.ts b/packages/store/src/-private/utils/normalize-model-name.ts index 7fd5caacef5..dba54a8b1c1 100644 --- a/packages/store/src/-private/utils/normalize-model-name.ts +++ b/packages/store/src/-private/utils/normalize-model-name.ts @@ -1,21 +1,28 @@ -import { dasherize } from '@ember/string'; +import { deprecate } from '@ember/debug'; -/** - @module @ember-data/store -*/ +import { dasherize } from '@ember-data/request-utils/string'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; -/** - This method normalizes a modelName into the format Ember Data uses - internally by dasherizing it. +export function normalizeModelName(type: string): string { + if (DEPRECATE_NON_STRICT_TYPES) { + const result = dasherize(type); - @method normalizeModelName - @static - @public - @deprecated - @for @ember-data/store - @param {String} modelName - @return {String} normalizedModelName -*/ -export default function normalizeModelName(modelName: string): string { - return dasherize(modelName); + 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; } diff --git a/packages/store/src/-private/utils/uuid-polyfill.ts b/packages/store/src/-private/utils/uuid-polyfill.ts index e4e1a3d0fb9..108cfc1670c 100644 --- a/packages/store/src/-private/utils/uuid-polyfill.ts +++ b/packages/store/src/-private/utils/uuid-polyfill.ts @@ -9,13 +9,13 @@ type UUIDv4 = `${string}-${string}-${string}-${string}-${string}`; export default function installPolyfill() { const isFastBoot = typeof FastBoot !== 'undefined'; - const CRYPTO: Crypto = isFastBoot ? (FastBoot.require('crypto') as Crypto) : window.crypto; + const CRYPTO: Crypto = isFastBoot ? (FastBoot.require('crypto') as Crypto) : globalThis.crypto; if (!CRYPTO.randomUUID) { // we might be able to optimize this by requesting more bytes than we need at a time const rng = function (): Uint8Array { // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto - let rnds8 = new Uint8Array(16); + const rnds8 = new Uint8Array(16); if (!CRYPTO.getRandomValues && !isFastBoot) { throw new Error(`Unable to generate bytes for UUID`); @@ -36,7 +36,7 @@ export default function installPolyfill() { } const bytesToUuid = function (buf: Uint8Array): UUIDv4 { - let bth = byteToHex; + const bth = byteToHex; // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4 return [ bth[buf[0]], @@ -63,7 +63,7 @@ export default function installPolyfill() { }; CRYPTO.randomUUID = function uuidv4(): UUIDv4 { - let rnds = rng(); + const rnds = rng(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = (rnds[6] & 0x0f) | 0x40; diff --git a/ember-data-types/overview.ts b/packages/store/src/-types/overview.ts similarity index 100% rename from ember-data-types/overview.ts rename to packages/store/src/-types/overview.ts diff --git a/packages/store/src/-types/q/cache-capabilities-manager.ts b/packages/store/src/-types/q/cache-capabilities-manager.ts new file mode 100644 index 00000000000..43abf1eb8e1 --- /dev/null +++ b/packages/store/src/-types/q/cache-capabilities-manager.ts @@ -0,0 +1,119 @@ +import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; + +import type { IdentifierCache } from '../../-private/caches/identifier-cache'; +import type { NotificationType } from '../../-private/managers/notification-manager'; +import type { SchemaService } from './schema-service'; + +/** + @module @ember-data/store +*/ + +/** + * CacheCapabilitiesManager provides encapsulated API access to the minimal + * subset of the Store's functionality that Cache implementations + * should interact with. It is provided to the Store's `createCache` hook. + * + * Cache implementations should not need more than this API provides. + * + * This class cannot be directly instantiated. + * + * @class CacheCapabilitiesManager + * @public + */ +export type CacheCapabilitiesManager = { + /** + * Provides access to the IdentifierCache instance + * for this Store instance. + * + * The IdentifierCache can be used to peek, generate or + * retrieve a stable unique identifier for any resource. + * + * @property {IdentifierCache} identifierCache + * @public + */ + identifierCache: IdentifierCache; + + /** + * DEPRECATED - use the schema property + * + * Provides access to the SchemaService instance + * for this Store instance. + * + * The SchemaService can be used to query for + * information about the schema of a resource. + * + * @method getSchemaDefinitionService + * @deprecated + * @public + */ + getSchemaDefinitionService(): SchemaService; + + /** + * Provides access to the SchemaService instance + * for this Store instance. + * + * The SchemaService can be used to query for + * information about the schema of a resource. + * + * @property schema + * @public + */ + schema: SchemaService; + + /** + * Update the `id` for the record corresponding to the identifier + * This operation can only be done for records whose `id` is `null`. + * + * @method setRecordId + * @param {StableRecordIdentifier} identifier; + * @param {string} id; + * @public + */ + setRecordId(identifier: StableRecordIdentifier, id: string): void; + + /** + * Signal to the store that the specified record may be considered fully + * removed from the cache. Generally this means that not only does no + * data exist for the identified resource, no known relationships still + * point to it either. + * + * @method disconnectRecord + * @param {StableRecordIdentifier} identifier + * @public + */ + disconnectRecord(identifier: StableRecordIdentifier): void; + + /** + * Use this method to determine if the Store has an instantiated record associated + * with an identifier. + * + * @method hasRecord + * @param identifier + * @return {boolean} + * @public + */ + hasRecord(identifier: StableRecordIdentifier): boolean; + + /** + * Notify subscribers of the NotificationManager that cache state has changed. + * + * `attributes` and `relationships` do not require a key, but if one is specified it + * is assumed to be the name of the attribute or relationship that has been updated. + * + * No other namespaces currently expect the `key` argument. + * + * @method notifyChange + * @param {StableRecordIdentifier} identifier + * @param {'attributes' | 'relationships' | 'identity' | 'errors' | 'meta' | 'state'} namespace + * @param {string|undefined} key + * @public + */ + 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 + ): void; +}; diff --git a/packages/store/src/-types/q/ds-model.ts b/packages/store/src/-types/q/ds-model.ts new file mode 100644 index 00000000000..28df41767c6 --- /dev/null +++ b/packages/store/src/-types/q/ds-model.ts @@ -0,0 +1,33 @@ +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; +import type { LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +export type KeyOrString = keyof T & string extends never ? string : keyof T & string; + +/** + * Minimum subset of static schema methods and properties on the + * "model" class. + * + * Only used when using the legacy schema-service implementation + * for @ember-data/model or when wrapping schema for legacy + * Adapters/Serializers. + * + * @typedoc + */ +export interface ModelSchema { + modelName: T extends TypedRecordInstance ? TypeFromInstance : string; + fields: Map, 'attribute' | 'belongsTo' | 'hasMany'>; + attributes: Map, LegacyAttributeField>; + relationshipsByName: Map, LegacyRelationshipSchema>; + eachAttribute>( + callback: (this: ModelSchema, key: K, attribute: LegacyAttributeField) => void, + binding?: T + ): void; + eachRelationship>( + callback: (this: ModelSchema, key: K, relationship: LegacyRelationshipSchema) => void, + binding?: T + ): void; + eachTransformedAttribute>( + callback: (this: ModelSchema, key: K, type: string | null) => void, + binding?: T + ): void; +} diff --git a/packages/store/src/-types/q/identifier.ts b/packages/store/src/-types/q/identifier.ts new file mode 100644 index 00000000000..193e4b0f6cc --- /dev/null +++ b/packages/store/src/-types/q/identifier.ts @@ -0,0 +1,195 @@ +/** + @module @ember-data/store +*/ + +import type { ImmutableRequestInfo } from '@ember-data/request'; +import type { IdentifierBucket, StableIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; + +/** + Configures how unique identifier lid strings are generated by @ember-data/store. + + This configuration MUST occur prior to the store instance being created. + + Takes a method which can expect to receive various data as its first argument + and the name of a bucket as its second argument. + + Currently there are two buckets, 'record' and 'document'. + + ### Resource (`Record`) Identity + + If the bucket is `record` the method must return a unique (to at-least + the given bucket) string identifier for the given data as a string to be + used as the `lid` of an `Identifier` token. + + This method will only be called by either `getOrCreateRecordIdentifier` or + `createIdentifierForNewRecord` when an identifier for the supplied data + is not already known via `lid` or `type + id` combo and one needs to be + generated or retrieved from a proprietary cache. + + `data` will be the same data argument provided to `getOrCreateRecordIdentifier` + and in the `createIdentifierForNewRecord` case will be an object with + only `type` as a key. + + ```ts + import { setIdentifierGenerationMethod } from '@ember-data/store'; + + export function initialize(applicationInstance) { + // note how `count` here is now scoped to the application instance + // for our generation method by being inside the closure provided + // by the initialize function + let count = 0; + + setIdentifierGenerationMethod((resource, bucket) => { + return resource.lid || `my-key-${count++}`; + }); + } + + export default { + name: 'configure-ember-data-identifiers', + initialize + }; + ``` + + ### Document Identity + + If the bucket is `document` the method will receive the associated + immutable `request` passed to `store.request` as its first argument + and should return a unique string for the given request if the document + should be cached, and `null` if it should not be cached. + + Note, the request result will still be passed to the cache via `Cache.put`, + but caches should take this as a signal that the document should not itself + be cached, while its contents may still be used to update other cache state. + + The presence of `cacheOptions.key` on the request will take precedence + for the document cache key, and this method will not be called if it is + present. + + The default method implementation for this bucket is to return `null` + for all requests whose method is not `GET`, and to return the `url` for + those where it is. + + This means that queries via `POST` MUST provide `cacheOptions.key` or + implement this hook. + + ⚠️ Caution: Requests that do not have a `method` assigned are assumed to be `GET` + + @method setIdentifierGenerationMethod + @for @ember-data/store + @param method + @public + @static +*/ +export interface GenerationMethod { + (data: ImmutableRequestInfo, bucket: 'document'): string | null; + (data: unknown | { type: string }, bucket: 'record'): string; + (data: unknown, bucket: IdentifierBucket): string | null; +} + +/** + Configure a callback for when the identifier cache encounters new resource + data for an existing resource. + + This configuration MUST occur prior to the store instance being created. + + ```js + import { setIdentifierUpdateMethod } from '@ember-data/store'; + ``` + + Takes a method which can expect to receive an existing `Identifier` alongside + some new data to consider as a second argument. This is an opportunity + for secondary lookup tables and caches associated with the identifier + to be amended. + + This method is called everytime `updateRecordIdentifier` is called and + with the same arguments. It provides the opportunity to update secondary + lookup tables for existing identifiers. + + It will always be called after an identifier created with `createIdentifierForNewRecord` + has been committed, or after an update to the `record` a `RecordIdentifier` + is assigned to has been committed. Committed here meaning that the server + has acknowledged the update (for instance after a call to `.save()`) + + If `id` has not previously existed, it will be assigned to the `Identifier` + prior to this `UpdateMethod` being called; however, calls to the parent method + `updateRecordIdentifier` that attempt to change the `id` or calling update + without providing an `id` when one is missing will throw an error. + + @method setIdentifierUpdateMethod + @for @ember-data/store + @param method + @public + @static +*/ + +export type UpdateMethod = { + (identifier: StableRecordIdentifier, newData: unknown, bucket: 'record'): void; + (identifier: StableIdentifier, newData: unknown, bucket: never): void; +}; + +/** + Configure a callback for when the identifier cache is going to release an identifier. + + This configuration MUST occur prior to the store instance being created. + + ```js + import { setIdentifierForgetMethod } from '@ember-data/store'; + ``` + + Takes method which can expect to receive an existing `Identifier` that should be eliminated + from any secondary lookup tables or caches that the user has populated for it. + + @method setIdentifierForgetMethod + @for @ember-data/store + @param method + @public + @static +*/ +export type ForgetMethod = (identifier: StableIdentifier | StableRecordIdentifier, bucket: IdentifierBucket) => void; + +/** + Configure a callback for when the identifier cache is being torn down. + + This configuration MUST occur prior to the store instance being created. + + ```js + import { setIdentifierResetMethod } from '@ember-data/store'; + ``` + + Takes a method which can expect to be called when the parent application is destroyed. + + If you have properly used a WeakMap to encapsulate the state of your customization + to the application instance, you may not need to implement the `resetMethod`. + + @method setIdentifierResetMethod + @for @ember-data/store + @param method + @public + @static +*/ +export type ResetMethod = () => void; + +/** + Configure a callback for when the identifier cache is generating a new + StableRecordIdentifier for a resource. + + This method controls the `type` and `id` that will be assigned to the + `StableRecordIdentifier` that is created. + + This configuration MUST occur prior to the store instance being created. + + ```js + import { setKeyInfoForResource } from '@ember-data/store'; + ``` + + @method setKeyInfoForResource + @for @ember-data/store + @param method + @public + @static + */ +export type KeyInfo = { + id: string | null; + type: string; +}; +export type KeyInfoMethod = (resource: unknown, known: StableRecordIdentifier | null) => KeyInfo; diff --git a/ember-data-types/q/promise-proxies.ts b/packages/store/src/-types/q/promise-proxies.ts similarity index 100% rename from ember-data-types/q/promise-proxies.ts rename to packages/store/src/-types/q/promise-proxies.ts diff --git a/packages/store/src/-types/q/record-data-json-api.ts b/packages/store/src/-types/q/record-data-json-api.ts new file mode 100644 index 00000000000..ff3cd3e7e66 --- /dev/null +++ b/packages/store/src/-types/q/record-data-json-api.ts @@ -0,0 +1,44 @@ +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { + CollectionResourceRelationship, + Link, + Links, + Meta, + SingleResourceRelationship, +} from '@warp-drive/core-types/spec/json-api-raw'; + +/** + @module @ember-data/store +*/ + +export type AttributesHash = Record; + +export interface JsonApiResource { + id?: string | null; + type?: string; + lid?: string; + attributes?: AttributesHash; + relationships?: Record; + meta?: Meta; + links?: Links; +} + +export interface JsonApiError { + id?: string; + title?: string; + detail?: string; + links?: { + about?: Link; + type?: Link; + }; + status?: string; + code?: string; + source?: { + pointer: string; + parameter?: string; + header?: string; + }; + meta?: Meta; +} + +export type JsonApiRelationship = SingleResourceRelationship | CollectionResourceRelationship; diff --git a/packages/store/src/-types/q/record-instance.ts b/packages/store/src/-types/q/record-instance.ts new file mode 100644 index 00000000000..6e8d04219bb --- /dev/null +++ b/packages/store/src/-types/q/record-instance.ts @@ -0,0 +1,27 @@ +/** + @module @ember-data/store +*/ + +/** + In EmberData, a "record instance" is a class instance used to present the data + for a single resource, transforming the resource's cached raw data into a form + that is useful for the application. + + Since every application's needs are different, EmberData does not assume to know + what the shape of the record instance should be. Instead, it provides a way to + define the record instance's via the `instantiateRecord` hook on the store. + + Thus for most purposes the `RecordInstance` type is "opaque" to EmberData, and + should be treated as "unknown" by the library. + + Wherever possible, if typing an API that is consumer facing, instead of using + OpaqueRecordInstance, we should prefer to use a generic and check if the generic + extends `TypedRecordInstance`. This allows consumers to define their own record + instance types and not only have their types flow through EmberData APIs, but + also allows EmberData to provide typechecking and intellisense for the record + based on a special symbol prsent on record instances that implement the + `TypedRecordInstance` interface. + + @typedoc +*/ +export type OpaqueRecordInstance = unknown; diff --git a/packages/store/src/-types/q/schema-service.ts b/packages/store/src/-types/q/schema-service.ts new file mode 100644 index 00000000000..3e960aa716b --- /dev/null +++ b/packages/store/src/-types/q/schema-service.ts @@ -0,0 +1,366 @@ +/** + @module @ember-data/store +*/ + +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { RecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + LegacyAttributeField, + LegacyBelongsToField, + LegacyHasManyField, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; + +export type AttributesSchema = Record; +export type RelationshipsSchema = Record; + +/** + * The SchemaService provides the ability to query for information about the structure + * of any resource type. + * + * Applications can provide any implementation of the SchemaService they please so long + * as it conforms to this interface. + * + * The design of the service means that schema information could be lazily populated, + * derived-on-demand, or progressively enhanced during the course of an application's runtime. + * The primary requirement is merely that any information the service needs to correctly + * respond to an inquest is available by the time it is asked. + * + * The `@ember-data/model` package provides an implementation of this service which + * makes use of your model classes as the source of information to respond to queries + * about resource schema. While this is useful, this may not be ideal for your application. + * For instance, Schema information could be sideloaded or pre-flighted for API calls, + * resulting in no need to bundle and ship potentially large and expensive JSON + * or large Javascript based Models to pull information from. + * + * To register a custom schema implementation, implement the store's `createSchemaService` + * hook to return an instance of your service. + * + * ```ts + * import Store from '@ember-data/store'; + * import CustomSchemas from './custom-schemas'; + * + * export default class extends Store { + * createSchemaService() { + * return new CustomSchemas(); + * } + * } + * ``` + * + * At runtime, both the `Store` and the `CacheCapabilitiesManager` provide + * access to this service via the `schema` property. + * + * ```ts + * export default class extends Component { + * @service store; + * + * get fields() { + * return this.store + * .schema + * .fields(this.args.dataType); + * } + * } + * ``` + * + * @class SchemaService + * @public + */ +export interface SchemaService { + /** + * DEPRECATED - use `hasResource` instead + * + * Queries whether the SchemaService recognizes `type` as a resource type + * + * @method doesTypeExist + * @public + * @deprecated + * @param {string} type + * @return {boolean} + */ + doesTypeExist?(type: string): boolean; + + /** + * Queries whether the SchemaService recognizes `type` as a resource type + * + * @method hasResource + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {boolean} + */ + hasResource(resource: { type: string } | StableRecordIdentifier): boolean; + + /** + * Queries whether the SchemaService recognizes `type` as a resource trait + * + * @method hasTrait + * @public + * @param {string} type + * @return {boolean} + */ + hasTrait(type: string): boolean; + + /** + * Queries whether the given resource has the given trait + * + * @method resourceHasTrait + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @param {string} trait + * @return {boolean} + */ + resourceHasTrait(resource: { type: string } | StableRecordIdentifier, trait: string): boolean; + + /** + * Queries for the fields of a given resource type or resource identity. + * + * Should error if the resource type is not recognized. + * + * @method fields + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {Map} + */ + fields(resource: { type: string } | StableRecordIdentifier): Map; + + /** + * Returns the transformation registered with the name provided + * by `field.type`. Validates that the field is a valid transformable. + * + * @method transformation + * @public + * @param {TransformableField|{ type: string }} field + * @returns {Transformation} + */ + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation; + + /** + * Returns the hash function registered with the name provided + * by `field.type`. Validates that the field is a valid HashField. + * + * @method hashFn + * @public + * @param {HashField|{ type: string }} field + * @returns {HashFn} + */ + hashFn(field: HashField | { type: string }): HashFn; + + /** + * Returns the derivation registered with the name provided + * by `field.type`. Validates that the field is a valid DerivedField. + * + * @method derivation + * @public + * @param {DerivedField|{ type: string }} field + * @returns {Derivation} + */ + derivation(field: DerivedField | { type: string }): Derivation; + + /** + * Returns the schema for the provided resource type. + * + * @method resource + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {ResourceSchema} + */ + resource(resource: { type: string } | StableRecordIdentifier): ResourceSchema; + + /** + * Enables registration of multiple ResourceSchemas at once. + * + * This can be useful for either pre-loading schema information + * or for registering schema information delivered by API calls + * or other sources just-in-time. + * + * @method registerResources + * @public + * @param schemas + */ + registerResources(schemas: ResourceSchema[]): void; + + /** + * Enables registration of a single ResourceSchema. + * + * This can be useful for either pre-loading schema information + * or for registering schema information delivered by API calls + * or other sources just-in-time. + * + * @method registerResource + * @public + * @param {ResourceSchema} schema + */ + registerResource(schema: ResourceSchema): void; + + /** + * Enables registration of a transformation. + * + * The transformation can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerTransformations + * @public + * @param {Transformation} transform + */ + registerTransformation(transform: Transformation): void; + + /** + * Enables registration of a derivation. + * + * The derivation can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerDerivations + * @public + * @param {Derivation} derivation + */ + registerDerivation(derivation: Derivation): void; + + /** + * Enables registration of a hashing function + * + * The hashing function can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerHashFn + * @public + * @param {HashFn} hashfn + */ + registerHashFn(hashFn: HashFn): void; + + /** + * DEPRECATED - use `fields` instead + * + * Returns definitions for all properties of the specified resource + * that are considered "attributes". Generally these are properties + * that are not related to book-keeping state on the client and do + * not represent a linkage to another resource. + * + * The return value should be a dictionary of key:value pairs + * where the `key` is the attribute or property's name and `value` + * is an object with at least the property `name` which should also + * match `key`. + * + * Optionally, this object may also specify `type`, which should + * be a string reference to a `transform`, and `options` which + * should be dictionary in which any key:value pairs are permissable. + * + * For instance, when using `@ember-data/model`, the following attribute + * definition: + * + * ```ts + * class extends Model { + * @attr('string', { defaultValue: 'hello' }) greeting; + * @attr('date') birthday; + * @attr firstName; + * } + * ``` + * + * Would be returned as: + * + * ```js + * { + * greeting: { name: 'greeting', type: 'string', options: { defaultValue: 'hello' } }, + * birthday: { name: 'birthday', type: 'date' }, + * firstName: { name: 'firstName' } + * } + * ``` + * + * @method attributesDefinitionFor + * @public + * @deprecated + * @param {RecordIdentifier|{ type: string }} identifier + * @return {AttributesSchema} + */ + attributesDefinitionFor?(identifier: RecordIdentifier | { type: string }): AttributesSchema; + + /** + * DEPRECATED - use `fields` instead + * + * Returns definitions for all properties of the specified resource + * that are considered "relationships". Generally these are properties + * that represent a linkage to another resource. + * + * The return value should be a dictionary of key:value pairs + * where the `key` is the relationship or property's name and `value` + * is an object with at least the following properties: + * + * - `name` which should also match the `key` used in the dictionary. + * - `kind` which should be either `belongsTo` or `hasMany` + * - `type` which should be the related resource's string "type" + * - `options` which should be a dictionary allowing any key but with + * at least the below keys present. + * + * - `options.async` a boolean representing whether data for this relationship is + * typically loaded on-demand. + * - `options.inverse` a string or null representing the field name / key of the + * corresponding relationship on the inverse resource. + * + * Additionally the following options properties are optional. See [Polymorphic Relationships](https://rfcs.emberjs.com/id/0793-polymporphic-relations-without-inheritance) + * + * - `options.polymorphic` a boolean representing whether multiple resource types + * can be used to satisfy this relationship. + * - `options.as` a string representing the abstract type that the concrete side of + * a relationship must specify when fulfilling a polymorphic inverse. + * + * For example, the following Model using @ember-data/model would generate this relationships + * definition by default: + * + * ```js + * class User extends Model { + * @belongsTo('user', { async: false, inverse: null }) bestFriend; + * @hasMany('user', { async: true, inverse: 'friends' }) friends; + * @hasMany('pet', { async: false, polymorphic: true, inverse: 'owner' }) pets; + * } + * ``` + * + * Which would be returned as + * + * ```js + * { + * bestFriend: { + * name: 'bestFriend', + * kind: 'belongsTo', + * type: 'user', + * options: { + * async: false, + * inverse: null + * } + * }, + * friends: { + * name: 'friends', + * kind: 'hasMany', + * type: 'user', + * options: { + * async: true, + * inverse: 'friends' + * } + * }, + * pets: { + * name: 'pets', + * kind: 'hasMany', + * type: 'pet', + * options: { + * async: false, + * polymorphic: true, + * inverse: 'owner' + * } + * }, + * } + * ``` + * + * @method relationshipsDefinitionFor + * @public + * @deprecated + * @param {RecordIdentifier|{ type: string }} identifier + * @return {RelationshipsSchema} + */ + relationshipsDefinitionFor?(identifier: RecordIdentifier | { type: string }): RelationshipsSchema; +} diff --git a/packages/store/src/-types/q/store.ts b/packages/store/src/-types/q/store.ts new file mode 100644 index 00000000000..6acdcbe360a --- /dev/null +++ b/packages/store/src/-types/q/store.ts @@ -0,0 +1,38 @@ +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Includes, TypedRecordInstance } from '@warp-drive/core-types/record'; + +export interface BaseFinderOptions { + reload?: boolean; + backgroundReload?: boolean; + include?: T extends TypedRecordInstance ? Includes[] : string | string[]; + adapterOptions?: Record; +} +export interface FindRecordOptions extends BaseFinderOptions { + /** + * Data to preload into the store before the request is made. + * This feature is *highly* discouraged and has no corresponding + * feature when using builders and handlers. + * + * Excepting relationships: the data should be in the form of a + * JSON object where the keys are fields on the record and the value + * is the raw value to be added to the cache. + * + * Relationships can either be provided as string IDs from which + * an identifier will be built base upon the relationship's expected + * resource type, or be record instances from which the identifier + * will be extracted. + * + * @typedoc + */ + preload?: Record; +} + +export type QueryOptions = { + [K in string | 'adapterOptions']?: K extends 'adapterOptions' ? Record : unknown; +}; + +export type FindAllOptions = BaseFinderOptions; +export type LegacyResourceQuery = { + include?: T extends TypedRecordInstance ? Includes[] : string | string[]; + [key: string]: Value | undefined; +}; diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 69b90437db7..c30dab7d595 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -36,7 +36,8 @@ * to store data **in-memory**, add a [Handler](https://api.emberjs.com/ember-data/release/classes/%3CInterface%3E%20Cache) to fetch data from a source, * and implement `instantiateRecord` to tell the store how to display the data for individual resources. * - * > **Note** If you are using the package `ember-data` then a JSON:API cache, RequestManager, LegacyNetworkHandler, + * > **Note** + * > If you are using the package `ember-data` then a JSON:API cache, RequestManager, LegacyNetworkHandler, * > and `instantiateRecord` are configured for you by default. * * ### Configuring A Cache @@ -62,7 +63,8 @@ * Now that we have a `cache` let's setup something to handle fetching * and saving data via our API. * - * > **Note** The `ember-data` package automatically includes and configures + * > **Note** + * > The `ember-data` package automatically includes and configures * > the `@ember-data/json-api` cache for you. * * ### Handling Requests @@ -71,7 +73,8 @@ * * To start, let's install the `RequestManager` from `@ember-data/request` and the basic `Fetch` handler from ``@ember-data/request/fetch`. * - * > **Note** If your app uses `GraphQL`, `REST` or different conventions for `JSON:API` than your cache expects, other handlers may better fit your data. You can author your own handler by creating one that conforms to the [handler interface](https://github.com/emberjs/data/tree/main/packages/request#handling-requests). + * > **Note** + * > If your app uses `GraphQL`, `REST` or different conventions for `JSON:API` than your cache expects, other handlers may better fit your data. You can author your own handler by creating one that conforms to the [handler interface](https://github.com/emberjs/data/tree/main/packages/request#handling-requests). * * ```ts * import Store from '@ember-data/store'; @@ -117,7 +120,7 @@ * * ### Presenting Data from the Cache * - * Now that we have a source and a cach for our data, we need to configure how + * Now that we have a source and a cache for our data, we need to configure how * the Store delivers that data back to our application. We do this via the hook * [instantiateRecord](https://api.emberjs.com/ember-data/release/classes/Store/methods/instantiateRecord%20(hook)?anchor=instantiateRecord%20(hook)), * which allows us to transform the data for a resource before handing it to the application. @@ -171,7 +174,8 @@ * implementations either to support enhanced features for only a subset of records or to * be able to incrementally migrate from one record/cache to another record or cache. * - * > **Note:** The `ember-data` package automatically includes the `@ember-data/model` + * > **Note** + * > The `ember-data` package automatically includes the `@ember-data/model` * > package and configures it for you. * * @module @ember-data/store @@ -180,12 +184,26 @@ export { Store as default, + type StoreRequestContext, CacheHandler, normalizeModelName, + type Document, + type CachePolicy, + type StoreRequestInput, + recordIdentifierFor, + storeFor, +} from './-private'; + +export type { + DocumentCacheOperation, + CacheOperation, + NotificationType, +} from './-private/managers/notification-manager'; + +export { setIdentifierGenerationMethod, setIdentifierUpdateMethod, setIdentifierForgetMethod, setIdentifierResetMethod, - recordIdentifierFor, - storeFor, -} from './-private'; + setKeyInfoForResource, +} from './-private/caches/identifier-cache'; diff --git a/packages/store/src/types.ts b/packages/store/src/types.ts new file mode 100644 index 00000000000..f7b530a4c19 --- /dev/null +++ b/packages/store/src/types.ts @@ -0,0 +1,10 @@ +export type { CacheCapabilitiesManager } from './-types/q/cache-capabilities-manager'; +export type { ModelSchema } from './-types/q/ds-model'; +export type { SchemaService } from './-types/q/schema-service'; +export type { + BaseFinderOptions, + FindRecordOptions, + LegacyResourceQuery, + QueryOptions, + FindAllOptions, +} from './-types/q/store'; diff --git a/packages/store/tsconfig.json b/packages/store/tsconfig.json new file mode 100644 index 00000000000..82ac4f0094d --- /dev/null +++ b/packages/store/tsconfig.json @@ -0,0 +1,61 @@ +{ + "include": ["src/**/*", "../../@types/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "experimentalDecorators": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../request" + }, + { + "path": "../tracking" + }, + { + "path": "../core-types" + }, + { + "path": "../build-config" + }, + { + "path": "../request-utils" + } + ] +} diff --git a/packages/store/vite.config.mjs b/packages/store/vite.config.mjs new file mode 100644 index 00000000000..ba7aaf03493 --- /dev/null +++ b/packages/store/vite.config.mjs @@ -0,0 +1,26 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +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( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/tracking/CHANGELOG.md b/packages/tracking/CHANGELOG.md new file mode 100644 index 00000000000..ce224c5ea6d --- /dev/null +++ b/packages/tracking/CHANGELOG.md @@ -0,0 +1,63 @@ +# @ember-data/tracking Changelog + +## v5.3.4 (2024-06-15) + +#### :memo: Documentation + +* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :bug: Bug Fix + +* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) +* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :memo: Documentation + +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) + +#### :rocket: Enhancement + +* [#9094](https://github.com/emberjs/data/pull/9094) feat: support legacy attribute behaviors in SchemaRecord ([@gitKrystan](https://github.com/gitKrystan)) +* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@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)) + +#### :house: Internal + +* [#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)) +* [#9028](https://github.com/emberjs/data/pull/9028) chore: more isolated types ([@runspired](https://github.com/runspired)) +* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/tracking/README.md b/packages/tracking/README.md index b44009e3ee5..9e0d2f4f934 100644 --- a/packages/tracking/README.md +++ b/packages/tracking/README.md @@ -1,7 +1,40 @@ -@ember-data/tracking -============================================================================ +

+ + +

-Tracking Primitives for controlling change notification of Tracked properties when working with EmberData +

Tracking Primitives for controlling change notification of Tracked properties when working with EmberData

+ +## Installation + +Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/) + +``` +pnpm add @ember-data/tracking +``` + +**Tagged Releases** + +- ![NPM Canary Version](https://img.shields.io/npm/v/%40ember-data/tracking/canary?label=%40canary&color=FFBF00) +- ![NPM Beta Version](https://img.shields.io/npm/v/%40ember-data/tracking/beta?label=%40beta&color=ff00ff) +- ![NPM Stable Version](https://img.shields.io/npm/v/%40ember-data/tracking/latest?label=%40latest&color=90EE90) +- ![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. diff --git a/packages/tracking/addon-main.cjs b/packages/tracking/addon-main.cjs new file mode 100644 index 00000000000..25b3a963d42 --- /dev/null +++ b/packages/tracking/addon-main.cjs @@ -0,0 +1,5 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +module.exports = addonShim(__dirname); diff --git a/packages/tracking/addon-main.js b/packages/tracking/addon-main.js deleted file mode 100644 index 13f812d930a..00000000000 --- a/packages/tracking/addon-main.js +++ /dev/null @@ -1,93 +0,0 @@ -const requireModule = require('@ember-data/private-build-infra/src/utilities/require-module'); -const getEnv = require('@ember-data/private-build-infra/src/utilities/get-env'); -const detectModule = require('@ember-data/private-build-infra/src/utilities/detect-module'); - -const pkg = require('./package.json'); - -module.exports = { - name: pkg.name, - - options: { - '@embroider/macros': { - setOwnConfig: {}, - }, - }, - - _emberDataConfig: null, - configureEmberData() { - if (this._emberDataConfig) { - return this._emberDataConfig; - } - const app = this._findHost(); - const isProd = /production/.test(process.env.EMBER_ENV); - const hostOptions = app.options?.emberData || {}; - const debugOptions = Object.assign( - { - LOG_PAYLOADS: false, - LOG_OPERATIONS: false, - LOG_MUTATIONS: false, - LOG_NOTIFICATIONS: false, - LOG_REQUESTS: false, - LOG_REQUEST_STATUS: false, - LOG_IDENTIFIERS: false, - LOG_GRAPH: false, - LOG_INSTANCE_CACHE: false, - }, - hostOptions.debug || {} - ); - - const HAS_DEBUG_PACKAGE = detectModule(require, '@ember-data/debug', __dirname, pkg); - const HAS_META_PACKAGE = detectModule(require, 'ember-data', __dirname, pkg); - - const includeDataAdapterInProduction = - typeof hostOptions.includeDataAdapterInProduction === 'boolean' - ? hostOptions.includeDataAdapterInProduction - : HAS_META_PACKAGE; - - const includeDataAdapter = HAS_DEBUG_PACKAGE ? (isProd ? includeDataAdapterInProduction : true) : false; - const DEPRECATIONS = require('@ember-data/private-build-infra/src/deprecations')(hostOptions.compatWith || null); - const FEATURES = require('@ember-data/private-build-infra/src/features')(isProd); - - const ALL_PACKAGES = requireModule('@ember-data/private-build-infra/virtual-packages/packages.js'); - const MACRO_PACKAGE_FLAGS = Object.assign({}, ALL_PACKAGES.default); - delete MACRO_PACKAGE_FLAGS['HAS_DEBUG_PACKAGE']; - - Object.keys(MACRO_PACKAGE_FLAGS).forEach((key) => { - MACRO_PACKAGE_FLAGS[key] = detectModule(require, MACRO_PACKAGE_FLAGS[key], __dirname, pkg); - }); - - // copy configs forward - const ownConfig = this.options['@embroider/macros'].setOwnConfig; - ownConfig.compatWith = hostOptions.compatWith || null; - ownConfig.debug = debugOptions; - ownConfig.deprecations = Object.assign(DEPRECATIONS, ownConfig.deprecations || {}, hostOptions.deprecations || {}); - ownConfig.features = Object.assign({}, FEATURES, ownConfig.features || {}, hostOptions.features || {}); - ownConfig.includeDataAdapter = includeDataAdapter; - ownConfig.packages = MACRO_PACKAGE_FLAGS; - ownConfig.env = getEnv(ownConfig); - - this._emberDataConfig = ownConfig; - return ownConfig; - }, - - included() { - this.configureEmberData(); - return this._super.included.call(this, ...arguments); - }, - - treeForVendor() { - return; - }, - treeForPublic() { - return; - }, - treeForStyles() { - return; - }, - treeForAddonStyles() { - return; - }, - treeForApp() { - return; - }, -}; diff --git a/packages/tracking/babel.config.js b/packages/tracking/babel.config.js deleted file mode 100644 index 944b6102d62..00000000000 --- a/packages/tracking/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const macros = require('@ember-data/private-build-infra/src/v2-babel-build-pack'); - -module.exports = { - plugins: [ - ...macros, - // '@embroider/macros/src/babel/macros-babel-plugin.js', - ['@babel/plugin-transform-runtime', { loose: true }], - ['@babel/plugin-transform-typescript', { allowDeclareFields: true }], - ['@babel/plugin-proposal-decorators', { legacy: true, loose: true }], - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -}; diff --git a/packages/tracking/babel.config.mjs b/packages/tracking/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/tracking/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/tracking/ember-data-logo-dark.svg b/packages/tracking/ember-data-logo-dark.svg new file mode 100644 index 00000000000..737a4aa4321 --- /dev/null +++ b/packages/tracking/ember-data-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/tracking/ember-data-logo-light.svg b/packages/tracking/ember-data-logo-light.svg new file mode 100644 index 00000000000..58ac3d4e544 --- /dev/null +++ b/packages/tracking/ember-data-logo-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/tracking/eslint.config.mjs b/packages/tracking/eslint.config.mjs new file mode 100644 index 00000000000..c9ecef455e9 --- /dev/null +++ b/packages/tracking/eslint.config.mjs @@ -0,0 +1,22 @@ +// @ts-check +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'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['src'], + allowedImports: ['@glimmer/tracking/primitives/cache', '@ember/-internals/metal', '@glimmer/validator'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), +]; diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 5b9582f8e2f..18c402d8342 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -13,7 +13,7 @@ "homepage": "https://github.com/emberjs/data", "bugs": "https://github.com/emberjs/data/issues", "engines": { - "node": "16.* || >= 18" + "node": ">= 18.20.4" }, "keywords": [ "ember-addon" @@ -22,54 +22,71 @@ "extends": "../../package.json" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { + "@warp-drive/build-config": { + "injected": true + }, + "@warp-drive/core-types": { "injected": true } }, "dependencies": { - "ember-cli-babel": "^7.26.11", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@embroider/macros": "^1.10.0" + "@embroider/macros": "^1.16.6", + "@warp-drive/build-config": "workspace:*" + }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "@warp-drive/core-types": "workspace:*" }, "files": [ - "addon-main.js", - "addon", + "unstable-preview-types", + "addon-main.cjs", + "dist", "README.md", "LICENSE.md", "ember-data-logo-dark.svg", "ember-data-logo-light.svg" ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "scripts": { - "build": "rollup --config && babel ./addon --out-dir addon --plugins=../private-build-infra/src/transforms/babel-plugin-transform-ext.js", - "start": "rollup --config --watch", - "prepack": "pnpm build", - "prepare": "pnpm build" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "ember-addon": { - "main": "addon-main.js", + "main": "addon-main.cjs", "type": "addon", - "version": 1 + "version": 2, + "externals": [ + "@ember/-internals", + "@ember/-internals/metal", + "@glimmer/validator" + ] }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/cli": "^7.21.0", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-transform-runtime": "^7.21.4", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/preset-env": "^7.21.4", - "@babel/preset-typescript": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@embroider/addon-dev": "^3.0.0", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "rollup": "^3.20.2", - "tslib": "^2.5.0", - "typescript": "^5.0.3", - "walk-sync": "^3.0.0" + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@glimmer/component": "^1.1.2", + "@glimmer/validator": "^0.92.3", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11" }, "ember": { "edition": "octane" } -} \ No newline at end of file +} diff --git a/packages/tracking/rollup.config.mjs b/packages/tracking/rollup.config.mjs deleted file mode 100644 index 34fe8a317de..00000000000 --- a/packages/tracking/rollup.config.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { Addon } from '@embroider/addon-dev/rollup'; -import babel from '@rollup/plugin-babel'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; - -const addon = new Addon({ - srcDir: 'src', - destDir: 'addon', -}); - -export default { - // This provides defaults that work well alongside `publicEntrypoints` below. - // You can augment this if you need to. - output: addon.output(), - - external: ['@embroider/macros'], - - plugins: [ - // These are the modules that users should be able to import from your - // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', '-private.js']), - - nodeResolve({ extensions: ['.ts'] }), - babel({ - extensions: ['.ts'], - babelHelpers: 'runtime', // we should consider "external", - }), - - // Remove leftover build artifacts when starting a new build. - addon.clean(), - ], -}; diff --git a/packages/tracking/src/-private.ts b/packages/tracking/src/-private.ts index 5af114b381b..18cfad380e3 100644 --- a/packages/tracking/src/-private.ts +++ b/packages/tracking/src/-private.ts @@ -1,4 +1,9 @@ -import { DEBUG } from '@ember-data/env'; +import { tagForProperty } from '@ember/-internals/metal'; +import { consumeTag, dirtyTag } from '@glimmer/validator'; + +import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { getOrSetGlobal, peekTransient, setTransient } from '@warp-drive/core-types/-private'; /** * This package provides primitives that allow powerful low-level @@ -15,53 +20,89 @@ import { DEBUG } from '@ember-data/env'; * @main @ember-data/tracking */ type OpaqueFn = (...args: unknown[]) => unknown; -export type Tag = { ref: null; t: boolean }; +type Tag = { ref: null; t: boolean }; type Transaction = { cbs: Set; - props: Set; - sub: Set; + props: Set; + sub: Set; parent: Transaction | null; }; -let TRANSACTION: Transaction | null = null; function createTransaction() { - let transaction: Transaction = { + const transaction: Transaction = { cbs: new Set(), props: new Set(), sub: new Set(), parent: null, }; + const TRANSACTION = peekTransient('TRANSACTION'); + if (TRANSACTION) { transaction.parent = TRANSACTION; } - TRANSACTION = transaction; + setTransient('TRANSACTION', transaction); +} + +function maybeConsume(tag: ReturnType | null): void { + if (tag) { + consumeTag(tag); + } } -export function subscribe(obj: Tag): void { +function maybeDirty(tag: ReturnType | null): void { + if (tag) { + // @ts-expect-error - we are using Ember's Tag not Glimmer's + dirtyTag(tag); + } +} + +/** + * If there is a current transaction, ensures that the relevant tag (and any + * array computed chains symbols, if applicable) will be consumed during the + * transaction. + * + * If there is no current transaction, will consume the tag(s) immediately. + * + * @internal + * @param obj + */ +export function subscribe(obj: Tag | Signal): void { + const TRANSACTION = peekTransient('TRANSACTION'); + if (TRANSACTION) { TRANSACTION.sub.add(obj); + } else if ('tag' in obj) { + if (DEPRECATE_COMPUTED_CHAINS) { + maybeConsume(obj['[]']); + maybeConsume(obj['@length']); + } + consumeTag(obj.tag); } else { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions obj.ref; } } -function updateRef(obj: Tag): void { +function updateRef(obj: Tag | Signal): void { if (DEBUG) { try { - obj.ref = null; + if ('tag' in obj) { + if (DEPRECATE_COMPUTED_CHAINS) { + maybeDirty(obj['[]']); + maybeDirty(obj['@length']); + } + // @ts-expect-error - we are using Ember's Tag not Glimmer's + dirtyTag(obj.tag); + } else { + obj.ref = null; + } } catch (e: unknown) { if (e instanceof Error) { - if (e.message.includes('You attempted to update `ref` on `Tag`')) { - e.message = e.message.replace( - 'You attempted to update `ref` on `Tag`', - // @ts-expect-error - `You attempted to update <${obj._debug_base}>.${obj._debug_prop}` // eslint-disable-line - ); - e.stack = e.stack?.replace( - 'You attempted to update `ref` on `Tag`', - // @ts-expect-error - `You attempted to update <${obj._debug_base}>.${obj._debug_prop}` // eslint-disable-line - ); + if (e.message.includes('You attempted to update `undefined`')) { + // @ts-expect-error + const key = `<${obj._debug_base}>.${obj.key}`; + e.message = e.message.replace('You attempted to update `undefined`', `You attempted to update ${key}`); + e.stack = e.stack?.replace('You attempted to update `undefined`', `You attempted to update ${key}`); const lines = e.stack?.split(`\n`); const finalLines: string[] = []; @@ -87,9 +128,9 @@ function updateRef(obj: Tag): void { } }); - const splitstr = '`ref` was first used:'; + const splitstr = '`undefined` was first used:'; const parts = e.message.split(splitstr); - parts.splice(1, 0, `Original Stack\n=============\n${finalLines.join(`\n`)}\n\n${splitstr}`); + parts.splice(1, 0, `Original Stack\n=============\n${finalLines.join(`\n`)}\n\n\`${key}\` was first used:`); e.message = parts.join(''); } @@ -97,51 +138,73 @@ function updateRef(obj: Tag): void { throw e; } } else { - obj.ref = null; + if ('tag' in obj) { + if (DEPRECATE_COMPUTED_CHAINS) { + maybeDirty(obj['[]']); + maybeDirty(obj['@length']); + } + // @ts-expect-error - we are using Ember's Tag not Glimmer's + dirtyTag(obj.tag); + } else { + obj.ref = null; + } } } function flushTransaction() { - let transaction = TRANSACTION!; - TRANSACTION = transaction.parent; + const transaction = peekTransient('TRANSACTION')!; + setTransient('TRANSACTION', transaction.parent); transaction.cbs.forEach((cb) => { cb(); }); - transaction.props.forEach((obj: Tag) => { + transaction.props.forEach((obj) => { // mark this mutation as part of a transaction obj.t = true; updateRef(obj); }); - transaction.sub.forEach((obj: Tag) => { - obj.ref; + transaction.sub.forEach((obj) => { + if ('tag' in obj) { + if (DEPRECATE_COMPUTED_CHAINS) { + maybeConsume(obj['[]']); + maybeConsume(obj['@length']); + } + consumeTag(obj.tag); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + obj.ref; + } }); } async function untrack() { - let transaction = TRANSACTION!; - TRANSACTION = transaction.parent; + const transaction = peekTransient('TRANSACTION')!; + setTransient('TRANSACTION', transaction.parent); // defer writes await Promise.resolve(); transaction.cbs.forEach((cb) => { cb(); }); - transaction.props.forEach((obj: Tag) => { + transaction.props.forEach((obj) => { // mark this mutation as part of a transaction obj.t = true; updateRef(obj); }); } -export function addToTransaction(obj: Tag): void { - if (TRANSACTION) { - TRANSACTION.props.add(obj); +export function addToTransaction(obj: Tag | Signal): void { + const transaction = peekTransient('TRANSACTION'); + + if (transaction) { + transaction.props.add(obj); } else { updateRef(obj); } } export function addTransactionCB(method: OpaqueFn): void { - if (TRANSACTION) { - TRANSACTION.cbs.add(method); + const transaction = peekTransient('TRANSACTION'); + + if (transaction) { + transaction.cbs.add(method); } else { method(); } @@ -161,7 +224,7 @@ export function addTransactionCB(method: OpaqueFn): void { * @static * @for @ember-data/tracking * @param method - * @returns result of invoking method + * @return result of invoking method */ export function untracked(method: T): ReturnType { createTransaction(); @@ -184,7 +247,7 @@ export function untracked(method: T): ReturnType { * @static * @for @ember-data/tracking * @param method - * @returns result of invoking method + * @return result of invoking method */ export function transact(method: T): ReturnType { createTransaction(); @@ -203,7 +266,7 @@ export function transact(method: T): ReturnType { * @static * @for @ember-data/tracking * @param method - * @returns a function that will invoke method in a transaction with any provided args and return its result + * @return a function that will invoke method in a transaction with any provided args and return its result */ export function memoTransact(method: T): (...args: unknown[]) => ReturnType { return function (...args: unknown[]) { @@ -213,3 +276,209 @@ export function memoTransact(method: T): (...args: unknown[] return ret as ReturnType; }; } + +export const Signals = getOrSetGlobal('Signals', Symbol('Signals')); + +/** + * use to add a signal property to the prototype of something. + * + * First arg is the thing to define on + * Second arg is the property name + * Third agg is the initial value of the property if any. + * + * for instance + * + * ```ts + * class Model {} + * defineSignal(Model.prototype, 'isLoading', false); + * ``` + * + * This is sort of like using a stage-3 decorator but works today + * while we are still on legacy decorators. + * + * e.g. it is equivalent to + * + * ```ts + * class Model { + * @signal accessor isLoading = false; + * } + * ``` + * + * @internal + */ +export function defineSignal(obj: T, key: string, v?: unknown) { + Object.defineProperty(obj, key, { + enumerable: true, + configurable: false, + get(this: T & { [Signals]: Map }) { + const signals = (this[Signals] = this[Signals] || new Map()); + const existing = signals.has(key); + const _signal = entangleSignal(signals, this, key); + if (!existing && v !== undefined) { + _signal.lastValue = v; + } + return _signal.lastValue; + }, + set(this: T & { [Signals]: Map }, value: unknown) { + const signals = (this[Signals] = this[Signals] || new Map()); + let _signal = signals.get(key); + if (!_signal) { + _signal = createSignal(this, key); + signals.set(key, _signal); + } + if (_signal.lastValue !== value) { + _signal.lastValue = value; + addToTransaction(_signal); + } + }, + }); +} + +export interface Signal { + /** + * Key on the associated object + * @internal + */ + key: string; + _debug_base?: string; + + /** + * Whether this signal is part of an active transaction. + * @internal + */ + t: boolean; + + /** + * Whether to "bust" the lastValue cache + * @internal + */ + shouldReset: boolean; + + /** + * The framework specific "signal" e.g. glimmer "tracked" + * or starbeam "cell" to consume/invalidate when appropriate. + * + * @internal + */ + tag: ReturnType; + + /** + * In classic ember, arrays must entangle a `[]` symbol + * in addition to any other tag in order for array chains to work. + * + * Note, this symbol MUST be the one that ember itself generates + * + * @internal + */ + '[]': ReturnType | null; + /** + * In classic ember, arrays must entangle a `@length` symbol + * in addition to any other tag in order for array chains to work. + * + * Note, this symbol MUST be the one that ember itself generates + * + * @internal + */ + '@length': ReturnType | null; + + /** + * The lastValue computed for this signal when + * a signal is also used for storage. + * @internal + */ + lastValue: unknown; +} + +export function createArrayTags(obj: T, signal: Signal) { + if (DEPRECATE_COMPUTED_CHAINS) { + signal['[]'] = tagForProperty(obj, '[]'); + signal['@length'] = tagForProperty(obj, 'length'); + } +} + +/** + * Create a signal for the key/object pairing. + * + * @internal + * @param obj Object we're creating the signal on + * @param key Key to create the signal for + * @return the signal + */ +export function createSignal(obj: T, key: string): Signal { + const _signal: Signal = { + key, + tag: tagForProperty(obj, key), + + t: false, + shouldReset: false, + '[]': null, + '@length': null, + lastValue: undefined, + }; + + if (DEBUG) { + function tryGet(prop: string): T1 | undefined { + try { + return obj[prop as keyof typeof obj] as unknown as T1; + } catch { + return; + } + } + const modelName = + tryGet('$type') ?? tryGet('modelName') ?? tryGet<{ modelName?: string }>('constructor')?.modelName ?? ''; + + const className = obj.constructor?.name ?? obj.toString?.() ?? 'unknown'; + _signal._debug_base = `${className}${modelName && !className.startsWith('SchemaRecord') ? `:${modelName}` : ''}`; + } + + return _signal; +} + +/** + * Create a signal for the key/object pairing and subscribes to the signal. + * + * Use when you need to ensure a signal exists and is subscribed to. + * + * @internal + * @param signals Map of signals + * @param obj Object we're creating the signal on + * @param key Key to create the signal for + * @return the signal + */ +export function entangleSignal(signals: Map, obj: T, key: string): Signal { + let _signal = signals.get(key); + if (!_signal) { + _signal = createSignal(obj, key); + signals.set(key, _signal); + } + subscribe(_signal); + return _signal; +} + +interface Signaler { + [Signals]: Map; +} + +export function getSignal(obj: T, key: string, initialState: boolean): Signal { + let signals = (obj as Signaler)[Signals]; + + if (!signals) { + signals = new Map(); + (obj as Signaler)[Signals] = signals; + } + + let _signal = signals.get(key); + if (!_signal) { + _signal = createSignal(obj, key); + _signal.shouldReset = initialState; + signals.set(key, _signal); + } + return _signal; +} + +export function peekSignal(obj: T, key: string): Signal | undefined { + const signals = (obj as Signaler)[Signals]; + if (signals) { + return signals.get(key); + } +} diff --git a/packages/tracking/src/index.ts b/packages/tracking/src/index.ts index 414a60fccd3..58cfe387712 100644 --- a/packages/tracking/src/index.ts +++ b/packages/tracking/src/index.ts @@ -1 +1,45 @@ +import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; + +import { assert } from '@warp-drive/build-config/macros'; + export { transact, memoTransact, untracked } from './-private'; + +// temporary so we can remove the glimmer and ember imports elsewhere +// eslint-disable-next-line no-restricted-imports +export { dependentKeyCompat as compat } from '@ember/object/compat'; + +export function cached( + target: T, + key: K, + descriptor: PropertyDescriptor +) { + // Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;` + assert( + 'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!', + target !== undefined + ); + assert( + `You attempted to use @cached on with ${arguments.length > 1 ? 'arguments' : 'an argument'} ( @cached(${Array.from( + arguments + ) + .map((d) => `'${d}'`) + .join( + ', ' + )}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}`, + typeof target === 'object' && typeof key === 'string' && typeof descriptor === 'object' && arguments.length === 3 + ); + assert( + `The @cached decorator must be applied to getters. '${key}' is not a getter.`, + typeof descriptor.get === 'function' + ); + + const caches = new WeakMap(); + // eslint-disable-next-line @typescript-eslint/unbound-method + const getter = descriptor.get; + descriptor.get = function () { + if (!caches.has(this)) caches.set(this, createCache(getter.bind(this))); + return getValue(caches.get(this) as Parameters[0]); + }; +} + +export { createCache, getValue }; diff --git a/packages/tracking/tsconfig.json b/packages/tracking/tsconfig.json new file mode 100644 index 00000000000..ca805301705 --- /dev/null +++ b/packages/tracking/tsconfig.json @@ -0,0 +1,45 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "declarationDir": "unstable-preview-types", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "rootDir": "src", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../build-config" + }, + { + "path": "../core-types" + } + ] +} diff --git a/packages/tracking/vite.config.mjs b/packages/tracking/vite.config.mjs new file mode 100644 index 00000000000..3eafa62858e --- /dev/null +++ b/packages/tracking/vite.config.mjs @@ -0,0 +1,17 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + '@glimmer/validator', + '@ember/-internals/metal', + '@glimmer/tracking/primitives/cache', + '@ember/object/compat', +]; +export const entryPoints = ['src/index.ts', 'src/-private.ts']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/packages/unpublished-eslint-rules/CHANGELOG.md b/packages/unpublished-eslint-rules/CHANGELOG.md new file mode 100644 index 00000000000..237fccedd29 --- /dev/null +++ b/packages/unpublished-eslint-rules/CHANGELOG.md @@ -0,0 +1,30 @@ +# eslint-plugin-ember-data-internal Changelog + +## v5.3.4 (2024-06-15) + +#### :rocket: Enhancement + +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :house: Internal + +* [#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)) +* [#8923](https://github.com/emberjs/data/pull/8923) chore: prepare files for new eslint plugin ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + diff --git a/packages/unpublished-eslint-rules/package.json b/packages/unpublished-eslint-rules/package.json index 47c53b1a3e0..087147bc3e1 100644 --- a/packages/unpublished-eslint-rules/package.json +++ b/packages/unpublished-eslint-rules/package.json @@ -1,5 +1,5 @@ { - "name": "eslint-plugin-ember-data", + "name": "eslint-plugin-ember-data-internal", "main": "./src/index.js", "version": "4.12.8", "private": true, @@ -10,5 +10,7 @@ }, "volta": { "extends": "../../package.json" - } -} \ No newline at end of file + }, + "devDependencies": {}, + "scripts": {} +} diff --git a/packages/unpublished-test-infra/CHANGELOG.md b/packages/unpublished-test-infra/CHANGELOG.md new file mode 100644 index 00000000000..ae8972d590d --- /dev/null +++ b/packages/unpublished-test-infra/CHANGELOG.md @@ -0,0 +1,53 @@ +# @ember-data/unpublished-test-infra Changelog + +## v5.3.4 (2024-06-15) + +#### :rocket: Enhancement + +* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) +* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) +* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) +* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) +* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) +* [#9365](https://github.com/emberjs/data/pull/9365) chore: remove unneeded infra tests ([@runspired](https://github.com/runspired)) + +#### Committers: (1) + +Chris Thoburn ([@runspired](https://github.com/runspired)) + +For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md) + +## v5.3.1 (2024-02-24) + +#### :rocket: Enhancement + +* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) +* [#9095](https://github.com/emberjs/data/pull/9095) feat (internal): support legacy model behaviors in SchemaRecord legacy mode ([@runspired](https://github.com/runspired)) + +#### :house: Internal + +* [#9101](https://github.com/emberjs/data/pull/9101) chore: Type check test files ([@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)) +* [#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)) +* [#9057](https://github.com/emberjs/data/pull/9057) Add eslint-plugin-n to eslint config for node files ([@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)) +* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) +* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@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)) +* [#8906](https://github.com/emberjs/data/pull/8906) feat: expand mock-server capabilities, add to main tests ([@runspired](https://github.com/runspired)) + +#### Committers: (2) + +Chris Thoburn ([@runspired](https://github.com/runspired)) +Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) + diff --git a/packages/unpublished-test-infra/addon-main.cjs b/packages/unpublished-test-infra/addon-main.cjs new file mode 100644 index 00000000000..1e19e33d528 --- /dev/null +++ b/packages/unpublished-test-infra/addon-main.cjs @@ -0,0 +1,13 @@ +'use strict'; + +const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs'); + +const addon = addonShim(__dirname); +addon.options = addon.options || {}; +addon.options['@embroider/macros'] = addon.options['@embroider/macros'] || {}; +addon.options['@embroider/macros'].setOwnConfig = { + VERSION: require('./package.json').version, + ASSERT_ALL_DEPRECATIONS: Boolean(process.env.ASSERT_ALL_DEPRECATIONS), +}; + +module.exports = addon; diff --git a/packages/unpublished-test-infra/addon-test-support/assert-all-deprecations.js b/packages/unpublished-test-infra/addon-test-support/assert-all-deprecations.js deleted file mode 100644 index 1e94c678f7c..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/assert-all-deprecations.js +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint no-console:"off" */ -import QUnit from 'qunit'; - -import config from 'ember-get-config'; - -import { DEBUG } from '@ember-data/env'; - -const { ASSERT_ALL_DEPRECATIONS } = config; - -const ALL_ASSERTED_DEPRECATIONS = {}; - -function pushDeprecation(deprecation) { - if (deprecation in ALL_ASSERTED_DEPRECATIONS) { - ALL_ASSERTED_DEPRECATIONS[deprecation]++; - } else { - ALL_ASSERTED_DEPRECATIONS[deprecation] = 1; - } -} - -export default function configureAssertAllDeprecations() { - QUnit.begin(() => { - function assertAllDeprecations(assert) { - if (typeof assert.test.expected === 'number') { - assert.test.expected += 1; - } - assert.expectNoDeprecation(undefined, undefined, (deprecation) => { - // only assert EmberData deprecations - const id = deprecation.options.id.toLowerCase(); - const isEmberDataDeprecation = - id.includes('ds.') || - id.includes('emberdata') || - id.includes('ember-data') || - id.includes('mismatched-inverse-relationship-data-from-payload'); - - if (!isEmberDataDeprecation) { - if (ASSERT_ALL_DEPRECATIONS) { - pushDeprecation((deprecation.options && deprecation.options.id) || deprecation); - } else { - console.count(deprecation.options.id); - console.warn('Detected Non-Ember-Data Deprecation:', deprecation.message, deprecation.options.stacktrace); - } - } - - return ASSERT_ALL_DEPRECATIONS ? true : isEmberDataDeprecation; - }); - } - // ensure we don't regress quietly - // this plays nicely with `expectDeprecation` - if (DEBUG) { - QUnit.config.modules.forEach((mod) => { - const hooks = (mod.hooks.afterEach = mod.hooks.afterEach || []); - - if (mod.tests.length !== 0) { - hooks.unshift(assertAllDeprecations); - } - }); - } - }); - - QUnit.done(function () { - if (ASSERT_ALL_DEPRECATIONS) { - QUnit.config.deprecations = ALL_ASSERTED_DEPRECATIONS; - } - }); -} diff --git a/packages/unpublished-test-infra/addon-test-support/deep-copy.js b/packages/unpublished-test-infra/addon-test-support/deep-copy.js deleted file mode 100644 index eae8a64936a..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/deep-copy.js +++ /dev/null @@ -1,53 +0,0 @@ -export default function deepCopy(obj) { - return _deepCopy(obj, new WeakMap()); -} - -function isPrimitive(value) { - return typeof value !== 'object' || value === null; -} - -function _deepCopy(oldObject, seen) { - if (Array.isArray(oldObject)) { - return copyArray(oldObject, seen); - } else if (!isPrimitive(oldObject)) { - return copyObject(oldObject, seen); - } else { - return oldObject; - } -} - -function copyObject(oldObject, seen) { - let newObject = {}; - - Object.keys(oldObject).forEach((key) => { - let value = oldObject[key]; - let newValue = isPrimitive(value) ? value : seen.get(value); - - if (value && newValue === undefined) { - newValue = newObject[key] = _deepCopy(value, seen); - seen.set(value, newValue); - } - - newObject[key] = newValue; - }); - - return newObject; -} - -function copyArray(oldArray, seen) { - let newArray = []; - - for (let i = 0; i < oldArray.length; i++) { - let value = oldArray[i]; - let newValue = isPrimitive(value) ? value : seen.get(value); - - if (value && newValue === undefined) { - newValue = newArray[i] = _deepCopy(value, seen); - seen.set(value, newValue); - } - - newArray[i] = newValue; - } - - return newArray; -} diff --git a/packages/unpublished-test-infra/addon-test-support/deprecated-test.js b/packages/unpublished-test-infra/addon-test-support/deprecated-test.js deleted file mode 100644 index 19ceee153b9..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/deprecated-test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { skip, test } from 'qunit'; - -import { DEBUG } from '@ember-data/env'; -import VERSION, { COMPAT_VERSION } from '@ember-data/unpublished-test-infra/test-support/version'; - -// small comparison function for major and minor semver values -function gte(EDVersion, DeprecationVersion) { - let _edv = EDVersion.split('.'); - let _depv = DeprecationVersion.split('.'); - // compare major - let major = +_edv[0] >= +_depv[0]; - // compare minor - let minor = +_edv[1] >= +_depv[1]; - return major || minor; -} - -export function deprecatedTest(testName, deprecation, testCallback) { - // '4.0' - if (typeof deprecation.until !== 'string' || deprecation.until.length < 3) { - throw new Error(`deprecatedTest expects { until } to be a version.`); - } - // 'ds.' - if (typeof deprecation.id !== 'string' || deprecation.id.length < 8) { - throw new Error(`deprecatedTest expects { id } to be a meaningful string`); - } - - async function interceptor(assert) { - await testCallback.call(this, assert); - if (DEBUG) { - if (typeof assert.test.expected === 'number') { - assert.test.expected += 1; - } - assert.expectDeprecation(deprecation); - } - } - - let testFn = test; - if (COMPAT_VERSION && gte(COMPAT_VERSION, VERSION)) { - testFn = skip; - } - if (!DEBUG) { - if (deprecation.debugOnly) { - testFn = skip; - } - } - - if (gte(VERSION, deprecation.until)) { - testFn(`DEPRECATION ${deprecation.id} until ${deprecation.until} | ${testName}`, interceptor); - } else { - testFn(`DEPRECATION ${deprecation.id} until ${deprecation.until} | ${testName}`, function (assert) { - if (deprecation.refactor === true) { - assert.ok(false, 'This test includes use of a deprecated feature that should now be refactored.'); - } else { - assert.ok(false, 'This test is for a deprecated feature whose time has come and should be removed'); - } - }); - } -} diff --git a/packages/unpublished-test-infra/addon-test-support/public-props.js b/packages/unpublished-test-infra/addon-test-support/public-props.js deleted file mode 100644 index 4da4f65576c..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/public-props.js +++ /dev/null @@ -1,11 +0,0 @@ -// publicProps(['prop1', 'prop2'], { prop1: val, prop2: val2, privatePro: val3 }) -> { prop1: val, prop2: val2 } -export default function publicProps(publicArgs, obj) { - return Object.assign.apply( - this, - [{}].concat( - Object.keys(obj).map((key) => ({ - [key]: Object.assign.apply(this, [{}].concat(publicArgs.map((prop) => ({ [prop]: obj[key][prop] })))), - })) - ) - ); -} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-assertion.ts b/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-assertion.ts deleted file mode 100644 index c98dff632ce..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-assertion.ts +++ /dev/null @@ -1,101 +0,0 @@ -import QUnit from 'qunit'; - -import type Assert from 'ember-data-qunit-asserts'; - -import { DEBUG } from '@ember-data/env'; - -import { checkMatcher } from './check-matcher'; -import isThenable from './utils/is-thenable'; - -let HAS_REGISTERED = false; - -interface AssertSomeResult { - result: boolean; - actual: string; - expected: string; - message: string; -} -interface AssertNoneResult { - result: boolean; - actual: string; - expected: ''; - message: string; -} - -function verifyAssertion(message: string, matcher: string | RegExp, label?: string): AssertSomeResult { - let passed = checkMatcher(message, matcher); - - return { - result: passed, - actual: message, - expected: String(matcher), - message: label || `Expected an assertion during the test`, - }; -} - -function verifyNoAssertion(message: string | undefined, label?: string): AssertNoneResult { - let passed = !message; - return { - result: passed, - actual: message || '', - expected: '', - message: label || `Expected no assertions during test`, - }; -} - -export function configureAssertionHandler() { - if (HAS_REGISTERED === true) { - throw new Error(`Attempting to re-register the assert-assertion handler`); - } - HAS_REGISTERED = true; - const assert: Assert = QUnit.assert; - - assert.expectAssertion = async function (cb: () => unknown, matcher: string | RegExp, label?: string): Promise { - let outcome; - - try { - let result = cb(); - if (isThenable(result)) { - await result; - } - outcome = verifyAssertion('', matcher, label); - } catch (e) { - outcome = verifyAssertion((e as Error).message, matcher, label); - } - - if (!DEBUG) { - outcome = { - result: true, - actual: '', - expected: '', - message: `Assertions do not run in production environments`, - }; - } - - this.pushResult(outcome); - }; - - assert.expectNoAssertion = async function (cb: () => unknown, label?: string) { - let outcome; - try { - let result = cb(); - if (isThenable(result)) { - await result; - } - outcome = verifyNoAssertion('', label); - } catch (e) { - outcome = verifyNoAssertion((e as Error).message, label); - } - - if (!DEBUG) { - outcome = { - result: true, - actual: '', - expected: '', - message: `Assertions do not run in production environments`, - }; - } - - this.pushResult(outcome); - }; -} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-deprecation.ts b/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-deprecation.ts deleted file mode 100644 index 52cddc1412d..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-deprecation.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { registerDeprecationHandler } from '@ember/debug'; -import { VERSION } from '@ember/version'; - -import QUnit from 'qunit'; -import semver from 'semver'; - -import { DEBUG } from '@ember-data/env'; -import type { Dict } from '@ember-data/types/q/utils'; - -import { checkMatcher } from './check-matcher'; -import isThenable from './utils/is-thenable'; - -function gte(version: string): boolean { - return semver.satisfies(semver.coerce(VERSION) as unknown as string, version); -} - -function lte(version: string): boolean { - return semver.satisfies(semver.coerce(VERSION) as unknown as string, version); -} - -let HAS_REGISTERED = false; -let DEPRECATIONS_FOR_TEST: FoundDeprecation[]; -let HANDLED_DEPRECATIONS_FOR_TEST: FoundDeprecation[]; - -interface DeprecationConfig { - id: string; - count?: number; - until: string; - message?: string | RegExp; - url?: string; - stacktrace?: string; - when?: Dict; -} -interface FoundDeprecation { - message: string; - options: { - id: string; - message?: string | RegExp; - until: string; - url?: string; - stacktrace?: string; - }; -} - -interface AssertSomeResult { - result: boolean; - actual: { id: string; count: number }; - expected: { id: string; count: number }; - message: string; -} -interface AssertNoneResult { - result: boolean; - actual: FoundDeprecation[]; - expected: FoundDeprecation[]; - message: string; -} - -// @ts-expect-error -Error.stackTraceLimit = 50; - -/** - * Returns a qunit assert result object which passes if the given deprecation - * `id` was found *exactly* `count` times. - * - * Fails if not found or found more or less than `count`. - * Fails if `until` not specified - * Optionally fails if `until` has been passed. - */ -function verifyDeprecation(config: DeprecationConfig, label?: string): AssertSomeResult { - // TODO optionally throw if `until` is the current version or older than current version - let matchedDeprecations = DEPRECATIONS_FOR_TEST.filter((deprecation) => { - let isMatched = deprecation.options.id === config.id; - if (!isMatched && config.message) { - // TODO when we hit this we should throw an error in the near future - isMatched = checkMatcher(deprecation.message, config.message); - } - return isMatched; - }); - DEPRECATIONS_FOR_TEST = DEPRECATIONS_FOR_TEST.filter((deprecation) => { - return matchedDeprecations.indexOf(deprecation) === -1; - }); - HANDLED_DEPRECATIONS_FOR_TEST.push(...matchedDeprecations); - - const expectedCount: number | 'ALL' = typeof config.count === 'number' || config.count === 'ALL' ? config.count : 1; - //@ts-expect-error TS having trouble realizing expectedCount can be 'ALL' - let passed = expectedCount === 'ALL' ? true : matchedDeprecations.length === expectedCount; - - return { - result: passed, - actual: { id: config.id, count: matchedDeprecations.length }, - //@ts-expect-error TS having trouble realizing expectedCount can be 'ALL' - expected: { id: config.id, count: expectedCount === 'ALL' ? matchedDeprecations.length : expectedCount }, - message: - label || - `Expected ${expectedCount} deprecation${expectedCount === 1 ? '' : 's'} for ${config.id} during test, ${ - passed ? expectedCount : 'but ' + matchedDeprecations.length - } deprecations were found.`, - }; -} - -function verifyNoDeprecation(filter?: (deprecation: FoundDeprecation) => boolean, label?: string): AssertNoneResult { - let UNHANDLED_DEPRECATIONS; - - if (filter) { - UNHANDLED_DEPRECATIONS = DEPRECATIONS_FOR_TEST.filter(filter); - DEPRECATIONS_FOR_TEST = DEPRECATIONS_FOR_TEST.filter((deprecation) => { - return UNHANDLED_DEPRECATIONS.indexOf(deprecation) === -1; - }); - } else { - UNHANDLED_DEPRECATIONS = DEPRECATIONS_FOR_TEST; - DEPRECATIONS_FOR_TEST = []; - } - - let deprecationStr = UNHANDLED_DEPRECATIONS.reduce((a, b) => { - return `${a}${b.message}\n`; - }, ''); - - let passed = UNHANDLED_DEPRECATIONS.length === 0; - - return { - result: passed, - actual: UNHANDLED_DEPRECATIONS, - expected: [], - message: - label || - `Expected 0 deprecations during test, ${ - passed ? '0' : 'but ' + UNHANDLED_DEPRECATIONS.length - } deprecations were found.\n${deprecationStr}`, - }; -} - -export function configureDeprecationHandler() { - if (HAS_REGISTERED === true) { - throw new Error(`Attempting to re-register the assert-deprecation handler`); - } - HAS_REGISTERED = true; - - QUnit.testStart(function () { - DEPRECATIONS_FOR_TEST = []; - HANDLED_DEPRECATIONS_FOR_TEST = []; - }); - - registerDeprecationHandler(function (message, options: DeprecationConfig /*, next*/) { - options.stacktrace = new Error().stack; - if (DEPRECATIONS_FOR_TEST) { - DEPRECATIONS_FOR_TEST.push({ message, options }); - } - // we do not call next to avoid spamming the console - }); - - // @ts-expect-error - QUnit.assert.expectDeprecation = async function ( - cb: () => unknown, - config: string | RegExp | DeprecationConfig, - label?: string - ): Promise { - let origDeprecations = DEPRECATIONS_FOR_TEST; - let callback: (() => unknown) | null = null; - - if (typeof cb !== 'function') { - config = cb; - callback = null; - } else { - callback = cb; - } - - if (typeof config === 'string' || config instanceof RegExp) { - config = { - id: 'unknown-data-deprecation', - count: 1, - message: config, - until: '4.0', - }; - } - - let skipAssert = true; - if (DEBUG) { - skipAssert = false; - if (!skipAssert && config.when) { - let libs = Object.keys(config.when); - for (let i = 0; i < libs.length; i++) { - let library = libs[i]; - let version = config.when[library]!; - - if (library !== 'ember') { - throw new Error(`when only supports setting a version for 'ember' currently.`); - } - - if (version.indexOf('<=') === 0) { - if (!lte(version)) { - skipAssert = true; - } - } else if (version.indexOf('>=') === 0) { - if (!gte(version)) { - skipAssert = true; - } - } else { - throw new Error( - `Expected a version range set to either >= or <= for the library ${library} when the deprecation ${config.id} is present, found ${version}.` - ); - } - } - } - } - - if (callback) { - DEPRECATIONS_FOR_TEST = []; - await callback(); - } - - let result; - if (skipAssert) { - result = { - result: true, - actual: { id: config.id, count: 0 }, - expected: { id: config.id, count: 0 }, - message: `Deprecations do not trigger in production environments`, - }; - } else { - result = verifyDeprecation(config, label); - } - - this.pushResult(result); - if (callback) { - DEPRECATIONS_FOR_TEST = origDeprecations.concat(DEPRECATIONS_FOR_TEST); - } - }; - QUnit.assert.expectNoDeprecation = async function ( - cb?: () => unknown, - label?: string, - filter?: (deprecation: FoundDeprecation) => boolean - ) { - let origDeprecations = DEPRECATIONS_FOR_TEST; - - if (cb) { - DEPRECATIONS_FOR_TEST = []; - let result = cb(); - if (isThenable(result)) { - await result; - } - } - - let result = verifyNoDeprecation(filter, label); - - if (!DEBUG) { - result = { - result: true, - actual: [], - expected: [], - message: `Deprecations do not trigger in production environments`, - }; - } - - this.pushResult(result); - DEPRECATIONS_FOR_TEST = origDeprecations.concat(DEPRECATIONS_FOR_TEST); - }; -} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-warning.ts b/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-warning.ts deleted file mode 100644 index 64ca540ce2c..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-warning.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { registerWarnHandler } from '@ember/debug'; - -import QUnit from 'qunit'; - -import { DEBUG } from '@ember-data/env'; - -import { checkMatcher } from './check-matcher'; -import isThenable from './utils/is-thenable'; - -let HAS_REGISTERED = false; -let WARNINGS_FOR_TEST: FoundWarning[]; -let HANDLED_WARNINGS_FOR_TEST: FoundWarning[]; - -interface WarningConfig { - id: string; - count?: number; - until?: string; - message?: string | RegExp; - url?: string; -} -interface FoundWarning { - message: string; - options: { - id: string; - message?: string; - until?: string; - url?: string; - }; -} - -interface AssertSomeResult { - result: boolean; - actual: { id: string; count: number }; - expected: { id: string; count: number }; - message: string; -} -interface AssertNoneResult { - result: boolean; - actual: FoundWarning[]; - expected: FoundWarning[]; - message: string; -} - -/** - * Returns a qunit assert result object which passes if the given warning - * `id` was found *exactly* `count` times. - * - * Fails if not found or found more or less than `count`. - * Fails if `until` not specified - * Optionally fails if `until` has been passed. - */ -function verifyWarning(config: WarningConfig, label?: string): AssertSomeResult { - // TODO optionally throw if `until` is the current version or older than current version - let matchedWarnings = WARNINGS_FOR_TEST.filter((warning) => { - let isMatched = warning.options.id === config.id; - if (!isMatched && config.message) { - // TODO when we hit this we should throw an error in the near future - isMatched = checkMatcher(warning.message, config.message); - } - return isMatched; - }); - WARNINGS_FOR_TEST = WARNINGS_FOR_TEST.filter((warning) => { - matchedWarnings.indexOf(warning) === -1; - }); - HANDLED_WARNINGS_FOR_TEST.push(...matchedWarnings); - - let expectedCount = typeof config.count === 'number' ? config.count : 1; - let passed = matchedWarnings.length === expectedCount; - - return { - result: passed, - actual: { id: config.id, count: matchedWarnings.length }, - expected: { id: config.id, count: expectedCount }, - message: - label || - `Expected ${expectedCount} warning${expectedCount === 1 ? '' : 's'} for ${config.id} during test, ${ - passed ? expectedCount : 'but ' + matchedWarnings.length - } warnings were found.`, - }; -} - -function verifyNoWarning(label?: string): AssertNoneResult { - const UNHANDLED_WARNINGS = WARNINGS_FOR_TEST; - WARNINGS_FOR_TEST = []; - - let warningStr = UNHANDLED_WARNINGS.reduce((a, b) => { - return `${a}${b.message}\n`; - }, ''); - - let passed = UNHANDLED_WARNINGS.length === 0; - - return { - result: passed, - actual: UNHANDLED_WARNINGS, - expected: [], - message: - label || - `Expected 0 warnings during test, ${ - passed ? '0' : 'but ' + UNHANDLED_WARNINGS.length - } warnings were found.\n${warningStr}`, - }; -} - -export function configureWarningHandler() { - if (HAS_REGISTERED === true) { - throw new Error(`Attempting to re-register the assert-warning handler`); - } - HAS_REGISTERED = true; - - QUnit.testStart(function () { - WARNINGS_FOR_TEST = []; - HANDLED_WARNINGS_FOR_TEST = []; - }); - - registerWarnHandler(function (message, options /*, next*/) { - if (WARNINGS_FOR_TEST) { - WARNINGS_FOR_TEST.push({ message, options }); - } - // we do not call next to avoid spamming the console - }); - - QUnit.assert.expectWarning = async function ( - cb: () => unknown, - config: string | RegExp | WarningConfig, - label?: string - ): Promise { - let origWarnings = WARNINGS_FOR_TEST; - let callback: (() => unknown) | null = null; - - if (typeof cb !== 'function') { - config = cb; - callback = null; - } else { - callback = cb; - } - - if (typeof config === 'string' || config instanceof RegExp) { - config = { - id: 'unknown-data-warning', - count: 1, - message: config, - until: '4.0', - }; - } - - if (callback) { - WARNINGS_FOR_TEST = []; - let result = callback(); - if (isThenable(result)) { - await result; - } - } - - let result = verifyWarning(config, label); - - if (!DEBUG) { - result = { - result: true, - actual: { id: config.id, count: 0 }, - expected: { id: config.id, count: 0 }, - message: `Warnings do not trigger in production environments`, - }; - } - - this.pushResult(result); - WARNINGS_FOR_TEST = origWarnings.concat(WARNINGS_FOR_TEST); - }; - - QUnit.assert.expectNoWarning = async function (cb, label?: string) { - let origWarnings = WARNINGS_FOR_TEST; - - if (cb) { - WARNINGS_FOR_TEST = []; - let result = cb(); - if (isThenable(result)) { - await result; - } - } - - let result = verifyNoWarning(label); - - if (!DEBUG) { - result = { - result: true, - actual: [], - expected: [], - message: `Warnings do not trigger in production environments`, - }; - } - - this.pushResult(result); - WARNINGS_FOR_TEST = origWarnings.concat(WARNINGS_FOR_TEST); - }; -} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/index.ts b/packages/unpublished-test-infra/addon-test-support/qunit-asserts/index.ts deleted file mode 100644 index 8993fa5e150..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { configureAssertionHandler } from './assert-assertion'; -import { configureDeprecationHandler } from './assert-deprecation'; -import { configureWarningHandler } from './assert-warning'; - -export default function configureAsserts() { - configureAssertionHandler(); - configureDeprecationHandler(); - configureWarningHandler(); -} diff --git a/packages/unpublished-test-infra/addon-test-support/test-in-debug.js b/packages/unpublished-test-infra/addon-test-support/test-in-debug.js deleted file mode 100644 index b645f0a1e46..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/test-in-debug.js +++ /dev/null @@ -1,11 +0,0 @@ -import { skip, test } from 'qunit'; - -import { DEBUG } from '@ember-data/env'; - -export default function testInDebug(label, callback) { - if (DEBUG) { - test(`[DEBUG-ONLY] ${label}`, callback); - } else { - skip(`[DEBUG-ONLY] ${label}`, callback); - } -} diff --git a/packages/unpublished-test-infra/addon-test-support/testem/custom-qunit-adapter.js b/packages/unpublished-test-infra/addon-test-support/testem/custom-qunit-adapter.js deleted file mode 100644 index 74ce7768e91..00000000000 --- a/packages/unpublished-test-infra/addon-test-support/testem/custom-qunit-adapter.js +++ /dev/null @@ -1,10 +0,0 @@ -import QUnit from 'qunit'; - -export default function customQUnitAdapter(socket) { - QUnit.done(function () { - let deprecations = QUnit.config.deprecations; - console.log('Deprecations: ', JSON.stringify(deprecations)); // eslint-disable-line no-console - - socket.emit('test-metadata', 'deprecations', deprecations); - }); -} diff --git a/packages/unpublished-test-infra/babel.config.mjs b/packages/unpublished-test-infra/babel.config.mjs new file mode 100644 index 00000000000..89e6a46e8a2 --- /dev/null +++ b/packages/unpublished-test-infra/babel.config.mjs @@ -0,0 +1,11 @@ +import { macros } from '@warp-drive/build-config/babel-macros'; + +export default { + plugins: [ + ...macros(), + [ + '@babel/plugin-transform-typescript', + { allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true }, + ], + ], +}; diff --git a/packages/unpublished-test-infra/ember-cli-build.js b/packages/unpublished-test-infra/ember-cli-build.js deleted file mode 100644 index 93f726983af..00000000000 --- a/packages/unpublished-test-infra/ember-cli-build.js +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); - -const compatWith = process.env.COMPAT_WITH || '0.0.0'; - -module.exports = function (defaults) { - let app = new EmberAddon(defaults, { - emberData: { - compatWith, - }, - babel: { - // this ensures that the same build-time code stripping that is done - // for library packages is also done for our tests and dummy app - plugins: [ - ...require('@ember-data/private-build-infra/src/debug-macros')({ - compatWith, - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }), - ], - }, - 'ember-cli-babel': { - throwUnlessParallelizable: true, - enableTypeScriptTransform: true, - }, - }); - - return app.toTree(); -}; diff --git a/packages/unpublished-test-infra/index.js b/packages/unpublished-test-infra/index.js deleted file mode 100644 index 9a4b16da32f..00000000000 --- a/packages/unpublished-test-infra/index.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; -// eslint-disable-next-line node/no-unpublished-require -const merge = require('broccoli-merge-trees'); -const version = require('@ember-data/private-build-infra/src/create-version-module'); -const addonBuildConfigForDataPackage = require('@ember-data/private-build-infra/src/addon-build-config-for-data-package'); - -const addonBaseConfig = addonBuildConfigForDataPackage(require('./package.json')); - -module.exports = Object.assign({}, addonBaseConfig, { - treeForAddonTestSupport(existingTree) { - const options = this.getEmberDataConfig(); - let compatVersion = options.compatWith; - let tree = merge([existingTree, version(compatVersion)]); - - return this.debugTree(this._super.treeForAddonTestSupport.call(this, tree), 'test-support'); - }, -}); diff --git a/packages/unpublished-test-infra/package.json b/packages/unpublished-test-infra/package.json index 1f972b3ea21..d78bd734555 100644 --- a/packages/unpublished-test-infra/package.json +++ b/packages/unpublished-test-infra/package.json @@ -13,67 +13,125 @@ }, "license": "MIT", "author": "", + "files": [ + "unstable-preview-types", + "addon-main.cjs", + "testem", + "dist" + ], + "exports": { + ".": { + "types": "./unstable-preview-types/index.d.ts", + "default": "./dist/index.js" + }, + "./testem/*": { + "default": "./testem/*.js" + }, + "./*": { + "types": "./unstable-preview-types/*.d.ts", + "default": "./dist/*.js" + } + }, "directories": { "doc": "doc", "test": "tests" }, "scripts": { - "test": "ember test" - }, - "dependencies": { - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@embroider/macros": "^1.10.0", - "broccoli-merge-trees": "^4.2.0", - "ember-auto-import": "^2.6.1", - "ember-cli-babel": "^7.26.11", - "ember-cli-blueprint-test-helpers": "^0.19.2", - "ember-get-config": "^2.1.1", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "semver": "^7.3.8", - "testem": "^3.10.1", - "webpack": "^5.77.0" + "build:pkg": "vite build;", + "prepack": "bun run build:pkg", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependenciesMeta": { - "@ember-data/private-build-infra": { - "injected": "true" + "@ember-data/store": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/diagnostic": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/build-config": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true } }, + "peerDependencies": { + "ember-source": "3.28.12 || ^4.0.4 || ^5.0.0 || ^6.0.0", + "qunit": "^2.20.1", + "testem": "^3.12.0", + "@ember-data/request": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/diagnostic": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@ember/test-helpers": "3.3.0 || ^4.0.4" + }, + "peerDependenciesMeta": { + "qunit": { + "optional": true + }, + "testem": { + "optional": true + }, + "@warp-drive/diagnostic": { + "optional": true + } + }, + "dependencies": { + "@embroider/macros": "^1.16.6", + "chalk": "^4.1.2", + "qunit": "^2.20.1", + "semver": "^7.6.3", + "@warp-drive/build-config": "workspace:*" + }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-typescript": "^7.24.1", + "@ember/test-helpers": "4.0.4", "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "@types/semver": "^7.3.13", - "ember-cli": "~4.11.0", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-test-loader": "^3.0.0", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0" + "@types/semver": "^7.5.8", + "@types/qunit": "2.19.10", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/diagnostic": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-source": "~5.12.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "qunit": "^2.20.1" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 18.20.4" }, "ember": { "edition": "octane" }, "ember-addon": { - "configPath": "tests/dummy/config" + "main": "addon-main.cjs", + "type": "addon", + "version": 2, + "preventDownleveling": true }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/packages/unpublished-test-infra/src/node-test-helpers/fixture.js b/packages/unpublished-test-infra/src/node-test-helpers/fixture.js deleted file mode 100644 index bf098c0d431..00000000000 --- a/packages/unpublished-test-infra/src/node-test-helpers/fixture.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const path = require('path'); - -const file = require('ember-cli-blueprint-test-helpers/chai').file; - -module.exports = function (directory, filePath) { - return file(path.join(directory, '../fixtures', filePath)); -}; diff --git a/packages/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest.js b/packages/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest.js deleted file mode 100644 index ca16d36cc44..00000000000 --- a/packages/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest.js +++ /dev/null @@ -1,16 +0,0 @@ -var fs = require('fs'); - -module.exports = function generateFakePackageManifest(name, version) { - if (!fs.existsSync('node_modules')) { - fs.mkdirSync('node_modules'); - } - if (!fs.existsSync('node_modules/' + name)) { - fs.mkdirSync('node_modules/' + name); - } - fs.writeFileSync( - 'node_modules/' + name + '/package.json', - JSON.stringify({ - version: version, - }) - ); -}; diff --git a/packages/unpublished-test-infra/src/node-test-helpers/setup-test-environment.js b/packages/unpublished-test-infra/src/node-test-helpers/setup-test-environment.js deleted file mode 100644 index f342256ec71..00000000000 --- a/packages/unpublished-test-infra/src/node-test-helpers/setup-test-environment.js +++ /dev/null @@ -1,26 +0,0 @@ -const { setEdition, clearEdition } = require('@ember/edition-utils'); - -function enableOctane() { - beforeEach(function () { - setEdition('octane'); - }); - - afterEach(function () { - clearEdition(); - }); -} - -function enableClassic() { - beforeEach(function () { - setEdition('classic'); - }); - - afterEach(function () { - clearEdition(); - }); -} - -module.exports = { - enableOctane, - enableClassic, -}; diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-all-deprecations.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-all-deprecations.ts new file mode 100644 index 00000000000..4d83aefcf13 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-all-deprecations.ts @@ -0,0 +1,72 @@ +import { DEBUG } from '@warp-drive/build-config/env'; + +import type { ExpandedHooks } from '.'; +import { FoundDeprecation } from './assert-deprecation'; + +import { getOwnConfig } from '@embroider/macros'; + +const { ASSERT_ALL_DEPRECATIONS } = getOwnConfig<{ ASSERT_ALL_DEPRECATIONS?: boolean }>(); + +const ALL_ASSERTED_DEPRECATIONS: Record = {}; + +function pushDeprecation(deprecation: string) { + if (deprecation in ALL_ASSERTED_DEPRECATIONS) { + ALL_ASSERTED_DEPRECATIONS[deprecation]++; + } else { + ALL_ASSERTED_DEPRECATIONS[deprecation] = 1; + } +} + +type Socket = { emit(type: string, name: string, data: unknown): void }; + +export function configureAssertAllDeprecations(hooks: ExpandedHooks) { + if (DEBUG) { + // @ts-expect-error Testem not typed + if (window.Testem) { + // @ts-expect-error Testem not typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + window.Testem.useCustomAdapter(function (socket: Socket) { + hooks.onSuiteFinish(function () { + if (ASSERT_ALL_DEPRECATIONS && Object.keys(ALL_ASSERTED_DEPRECATIONS).length) { + console.log('Deprecations: ', JSON.stringify(ALL_ASSERTED_DEPRECATIONS)); // eslint-disable-line no-console + + socket.emit('test-metadata', 'deprecations', ALL_ASSERTED_DEPRECATIONS); + } + }); + }); + } + + hooks.afterEach(async function assertAllDeprecations(assert) { + if (typeof assert.test.expected === 'number') { + assert.test.expected += 1; + } + + await assert.expectNoDeprecation( + undefined as unknown as () => void | Promise, + 'Expected no deprecations during test', + (deprecation: FoundDeprecation) => { + // only assert EmberData deprecations + const id = deprecation.options.id.toLowerCase(); + const isEmberDataDeprecation = + id.includes('ds.') || + id.includes('emberdata') || + id.includes('ember-data') || + id.includes('mismatched-inverse-relationship-data-from-payload'); + + if (!isEmberDataDeprecation) { + if (ASSERT_ALL_DEPRECATIONS) { + pushDeprecation(deprecation.options?.id ?? deprecation.message ?? 'unknown'); + } else { + // eslint-disable-next-line no-console + console.count(deprecation.options.id); + // eslint-disable-next-line no-console + console.warn('Detected Non-Ember-Data Deprecation:', deprecation.message, deprecation.options.stacktrace); + } + } + + return ASSERT_ALL_DEPRECATIONS ? true : isEmberDataDeprecation; + } + ); + }); + } +} diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-assertion.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-assertion.ts new file mode 100644 index 00000000000..ffcd4089460 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-assertion.ts @@ -0,0 +1,99 @@ +import type Assert from 'ember-data-qunit-asserts'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import { checkMatcher } from './check-matcher'; +import isThenable from './utils/is-thenable'; + +interface AssertSomeResult { + result: boolean; + actual: string; + expected: string; + message: string; +} +interface AssertNoneResult { + result: boolean; + actual: string; + expected: ''; + message: string; +} + +function verifyAssertion(message: string, matcher: string | RegExp, label?: string): AssertSomeResult { + let passed = checkMatcher(message, matcher); + + return { + result: passed, + actual: message, + expected: String(matcher), + message: label || `Expected an assertion during the test`, + }; +} + +function verifyNoAssertion(message: string | undefined, label?: string): AssertNoneResult { + let passed = !message; + return { + result: passed, + actual: message || '', + expected: '', + message: label || `Expected no assertions during test`, + }; +} + +export function configureAssertionHandler(assert: Assert) { + assert.expectAssertion = expectAssertion; + assert.expectNoAssertion = expectNoAssertion; +} + +async function expectAssertion( + this: Assert, + cb: () => unknown, + matcher: string | RegExp, + label?: string +): Promise { + let outcome: { result: boolean; actual: string; expected: string; message: string }; + + try { + let result = cb(); + if (isThenable(result)) { + await result; + } + outcome = verifyAssertion('', matcher, label); + } catch (e) { + outcome = verifyAssertion(e instanceof Error ? e.message : (e as string), matcher, label); + } + + if (!DEBUG) { + outcome = { + result: true, + actual: '', + expected: '', + message: `Assertions do not run in production environments`, + }; + } + + this.pushResult(outcome); +} + +async function expectNoAssertion(this: Assert, cb: () => unknown, label?: string) { + let outcome: { result: boolean; actual: string; expected: string; message: string }; + try { + let result = cb(); + if (isThenable(result)) { + await result; + } + outcome = verifyNoAssertion('', label); + } catch (e) { + outcome = verifyNoAssertion((e as Error).message, label); + } + + if (!DEBUG) { + outcome = { + result: true, + actual: '', + expected: '', + message: `Assertions do not run in production environments`, + }; + } + + this.pushResult(outcome); +} diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-better.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-better.ts new file mode 100644 index 00000000000..337a09ae553 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-better.ts @@ -0,0 +1,64 @@ +import type Assert from 'ember-data-qunit-asserts'; + +function refFromIndex(index: number, suffix: string): string { + return ``; +} +function getRefForItem(map: Map, item: T, index: number): string { + let ref = map.get(item); + if (ref === undefined) { + ref = refFromIndex(index, 'b'); + } + return ref; +} + +export function configureBetterAsserts(assert: Assert) { + assert.arrayStrictEquals = arrayStrictEquals; +} + +function arrayStrictEquals(this: Assert, actual: T[], expected: T[], message: string): void { + if (!Array.isArray(actual)) { + this.pushResult({ + result: false, + actual: false, + expected: true, + message: 'Expected the value for "actual" to be an array | ' + message, + }); + return; + } + if (!Array.isArray(expected)) { + this.pushResult({ + result: false, + actual: false, + expected: true, + message: 'Expected the value for "expected"" to be an array', + }); + return; + } + let passed = actual.length === expected.length; + + let actualRefs = new Map(); + let actualSerialized: string[] = actual.map((item, index) => { + let ref = refFromIndex(index, ''); + actualRefs.set(item, ref); + return ref; + }); + let expectedSerialized: string[] = expected.map((item, index) => { + return getRefForItem(actualRefs, item, index); + }); + + if (passed) { + for (let i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + passed = false; + break; + } + } + } + + this.pushResult({ + result: passed, + actual: actualSerialized, + expected: expectedSerialized, + message, + }); +} diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-deprecation.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-deprecation.ts new file mode 100644 index 00000000000..66ed6b6ef71 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-deprecation.ts @@ -0,0 +1,278 @@ +import { registerDeprecationHandler } from '@ember/debug'; +import { VERSION } from '@ember/version'; + +import semver from 'semver'; + +import type Assert from 'ember-data-qunit-asserts'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import { checkMatcher } from './check-matcher'; +import isThenable from './utils/is-thenable'; + +function gte(version: string): boolean { + return semver.satisfies(semver.coerce(VERSION) as unknown as string, version); +} + +function lte(version: string): boolean { + return semver.satisfies(semver.coerce(VERSION) as unknown as string, version); +} + +let HAS_REGISTERED = false; +let DEPRECATIONS_FOR_TEST: FoundDeprecation[]; +let HANDLED_DEPRECATIONS_FOR_TEST: FoundDeprecation[]; + +export interface DeprecationConfig { + id: string; + count?: number; + until: string; + message?: string | RegExp; + url?: string; + stacktrace?: string; + when?: Record; + logTraces?: boolean; +} +export interface FoundDeprecation { + message: string; + options: { + id: string; + message?: string | RegExp; + until: string; + url?: string; + stacktrace?: string; + }; +} + +interface AssertSomeResult { + result: boolean; + actual: { id: string; count: number }; + expected: { id: string; count: number }; + message: string; +} +interface AssertNoneResult { + result: boolean; + actual: FoundDeprecation[]; + expected: FoundDeprecation[]; + message: string; +} + +// Case is necessary outside of node types, which we sometimes resolve +// global times apparently missing this property. +(Error as unknown as { stackTraceLimit: number }).stackTraceLimit = 50; + +/** + * Returns a qunit assert result object which passes if the given deprecation + * `id` was found *exactly* `count` times. + * + * Fails if not found or found more or less than `count`. + * Fails if `until` not specified + * Optionally fails if `until` has been passed. + */ +function verifyDeprecation(config: DeprecationConfig, label?: string): AssertSomeResult { + // TODO optionally throw if `until` is the current version or older than current version + let matchedDeprecations = DEPRECATIONS_FOR_TEST.filter((deprecation) => { + let isMatched = deprecation.options.id === config.id; + if (!isMatched && config.message) { + // TODO when we hit this we should throw an error in the near future + isMatched = checkMatcher(deprecation.message, config.message); + } + return isMatched; + }); + DEPRECATIONS_FOR_TEST = DEPRECATIONS_FOR_TEST.filter((deprecation) => { + return !matchedDeprecations.includes(deprecation); + }); + HANDLED_DEPRECATIONS_FOR_TEST.push(...matchedDeprecations); + + const expectedCount: number | 'ALL' = typeof config.count === 'number' || config.count === 'ALL' ? config.count : 1; + //@ts-expect-error TS having trouble realizing expectedCount can be 'ALL' + let passed = expectedCount === 'ALL' ? true : matchedDeprecations.length === expectedCount; + + if (config.logTraces) { + matchedDeprecations.forEach((deprecation) => { + console.log(deprecation.options.stacktrace); + }); + } + + return { + result: passed, + actual: { id: config.id, count: matchedDeprecations.length }, + //@ts-expect-error TS having trouble realizing expectedCount can be 'ALL' + expected: { id: config.id, count: expectedCount === 'ALL' ? matchedDeprecations.length : expectedCount }, + message: + label || + `Expected ${expectedCount} deprecation${expectedCount === 1 ? '' : 's'} for ${config.id} during test, ${ + passed ? expectedCount : 'but ' + matchedDeprecations.length + } deprecations were found.`, + }; +} + +function verifyNoDeprecation(filter?: (deprecation: FoundDeprecation) => boolean, label?: string): AssertNoneResult { + let UNHANDLED_DEPRECATIONS: FoundDeprecation[] = []; + + if (filter) { + UNHANDLED_DEPRECATIONS = DEPRECATIONS_FOR_TEST.filter(filter); + DEPRECATIONS_FOR_TEST = DEPRECATIONS_FOR_TEST.filter((deprecation) => { + return !UNHANDLED_DEPRECATIONS.includes(deprecation); + }); + } else { + UNHANDLED_DEPRECATIONS = DEPRECATIONS_FOR_TEST; + DEPRECATIONS_FOR_TEST = []; + } + + let deprecationStr = UNHANDLED_DEPRECATIONS.reduce((a, b) => { + return `${a}${b.message}\n`; + }, ''); + + let passed = UNHANDLED_DEPRECATIONS.length === 0; + + return { + result: passed, + actual: UNHANDLED_DEPRECATIONS, + expected: [], + message: + label || + `Expected 0 deprecations during test, ${ + passed ? '0' : 'but ' + UNHANDLED_DEPRECATIONS.length + } deprecations were found.\n${deprecationStr}`, + }; +} + +export function configureDeprecationHandler(assert: Assert) { + if (!HAS_REGISTERED) { + registerDeprecationHandler(function (message, options /*, next*/) { + if (DEPRECATIONS_FOR_TEST && options) { + DEPRECATIONS_FOR_TEST.push({ + message, + options: { + id: options.id, + stacktrace: new Error().stack, + until: options.until, + url: options.url, + }, + }); + } + // we do not call next to avoid spamming the console + }); + HAS_REGISTERED = true; + } + + DEPRECATIONS_FOR_TEST = []; + HANDLED_DEPRECATIONS_FOR_TEST = []; + + assert.expectDeprecation = expectDeprecation; + assert.expectNoDeprecation = expectNoDeprecation; +} + +async function expectDeprecation( + this: Assert, + cb: DeprecationConfig | (() => void | Promise), + config?: string | RegExp | DeprecationConfig, + label?: string +): Promise { + let origDeprecations = DEPRECATIONS_FOR_TEST; + let callback: (() => unknown) | null = null; + + if (typeof cb !== 'function') { + label = config as string; + config = cb; + callback = null; + } else { + callback = cb; + } + + if (typeof config === 'string' || config instanceof RegExp) { + config = { + id: 'unknown-data-deprecation', + count: 1, + message: config, + until: '4.0', + }; + } + + if (typeof config !== 'object' || !config) { + throw new Error(`Expected a deprecation config object, got ${config}`); + } + + let skipAssert = true; + if (DEBUG) { + skipAssert = false; + if (!skipAssert && config.when) { + let libs = Object.keys(config.when); + for (let i = 0; i < libs.length; i++) { + let library = libs[i]; + let version = config.when[library]!; + + if (library !== 'ember') { + throw new Error(`when only supports setting a version for 'ember' currently.`); + } + + if (version.indexOf('<=') === 0) { + if (!lte(version)) { + skipAssert = true; + } + } else if (version.indexOf('>=') === 0) { + if (!gte(version)) { + skipAssert = true; + } + } else { + throw new Error( + `Expected a version range set to either >= or <= for the library ${library} when the deprecation ${config.id} is present, found ${version}.` + ); + } + } + } + } + + if (callback) { + DEPRECATIONS_FOR_TEST = []; + await callback(); + } + + let result: AssertSomeResult; + if (skipAssert) { + result = { + result: true, + actual: { id: config.id, count: 0 }, + expected: { id: config.id, count: 0 }, + message: `Deprecations do not trigger in production environments`, + }; + } else { + result = verifyDeprecation(config, label); + } + + this.pushResult(result); + if (callback) { + DEPRECATIONS_FOR_TEST = origDeprecations.concat(DEPRECATIONS_FOR_TEST); + } +} + +async function expectNoDeprecation( + this: Assert, + cb?: () => void | Promise, + label?: string, + filter?: (deprecation: FoundDeprecation) => boolean +) { + let origDeprecations = DEPRECATIONS_FOR_TEST; + + if (cb) { + DEPRECATIONS_FOR_TEST = []; + let result = cb(); + if (isThenable(result)) { + await result; + } + } + + let result = verifyNoDeprecation(filter, label); + + if (!DEBUG) { + result = { + result: true, + actual: [], + expected: [], + message: `Deprecations do not trigger in production environments`, + }; + } + + this.pushResult(result); + DEPRECATIONS_FOR_TEST = origDeprecations.concat(DEPRECATIONS_FOR_TEST); +} diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-notification.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-notification.ts new file mode 100644 index 00000000000..5597a9086a5 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-notification.ts @@ -0,0 +1,117 @@ +import { TestContext } from '@ember/test-helpers'; + +import type { StableRecordIdentifier } from '@warp-drive/core-types'; + +import type Assert from 'ember-data-qunit-asserts'; + +import type Store from '@ember-data/store'; +import type { DocumentCacheOperation, NotificationType } from '@ember-data/store'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; + +type Counter = { count: number }; +type NotificationStorage = Map< + StableDocumentIdentifier | StableRecordIdentifier | 'document' | 'resource', + Map> +>; + +function getCounter( + context: TestContext, + identifier: StableRecordIdentifier | StableDocumentIdentifier, + bucket: NotificationType | DocumentCacheOperation, + key: string | null +) { + const storage = (context as unknown as { _notifications: NotificationStorage })._notifications; + if (!storage) { + throw new Error(`setupNotifications must be called before calling notified`); + } + + let identifierStorage = storage.get(identifier); + if (!identifierStorage) { + identifierStorage = new Map(); + storage.set(identifier, identifierStorage); + } + + let bucketStorage = identifierStorage.get(bucket); + if (!bucketStorage) { + if (bucket === 'added' || bucket === 'removed' || bucket === 'updated' || bucket === 'state') { + bucketStorage = { count: 0 }; + } else { + bucketStorage = new Map(); + } + identifierStorage.set(bucket, bucketStorage); + } + + let counter: Counter; + if (bucketStorage instanceof Map) { + const _key = key || Symbol.for(bucket); + counter = bucketStorage.get(_key)!; + if (!counter) { + counter = { count: 0 }; + bucketStorage.set(_key, counter); + } + } else { + counter = bucketStorage; + } + + return counter; +} + +function clearNotifications(context: TestContext) { + const storage = (context as unknown as { _notifications: NotificationStorage })._notifications; + if (!storage) { + throw new Error(`setupNotifications must be called before calling notified`); + } + storage.clear(); +} + +function setupNotifications(context: TestContext, store: Store) { + (context as unknown as { _notifications: NotificationStorage })._notifications = new Map(); + + const notifications = store.notifications; + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalNotify = notifications.notify; + notifications.notify = function ( + identifier: StableRecordIdentifier | StableDocumentIdentifier, + bucket: NotificationType | DocumentCacheOperation, + key?: string + ) { + const counter = getCounter(context, identifier, bucket, key ?? null); + counter.count++; + + // @ts-expect-error TS is bad at curried overloads + return originalNotify.apply(notifications, [identifier, bucket, key]); + }; +} + +export function configureNotificationsAssert(this: TestContext, assert: Assert) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + + assert.watchNotifications = function (store?: Store) { + store = store ?? (context.owner.lookup('service:store') as unknown as Store); + setupNotifications(context, store); + }; + + assert.notified = function ( + this: Assert, + identifier: StableRecordIdentifier | StableDocumentIdentifier, + bucket: NotificationType | DocumentCacheOperation, + key: string | null, + count: number + ) { + const counter = getCounter(context, identifier, bucket, key); + + this.pushResult({ + result: counter.count === count, + actual: counter.count, + expected: count, + message: `Expected ${count} ${bucket} notifications for ${identifier.lid} ${key || ''}, got ${counter.count}`, + }); + + counter.count = 0; + }; + + assert.clearNotifications = function () { + clearNotifications(context); + }; +} diff --git a/packages/unpublished-test-infra/src/test-support/asserts/assert-warning.ts b/packages/unpublished-test-infra/src/test-support/asserts/assert-warning.ts new file mode 100644 index 00000000000..3239de15d85 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/assert-warning.ts @@ -0,0 +1,194 @@ +import { registerWarnHandler } from '@ember/debug'; + +import type Assert from 'ember-data-qunit-asserts'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import { checkMatcher } from './check-matcher'; +import isThenable from './utils/is-thenable'; + +let HAS_REGISTERED = false; +let WARNINGS_FOR_TEST: FoundWarning[]; +let HANDLED_WARNINGS_FOR_TEST: FoundWarning[]; + +export interface WarningConfig { + id: string; + count?: number; + until?: string; + message?: string | RegExp; + url?: string; +} +interface FoundWarning { + message: string; + options: { + id: string; + message?: string; + until?: string; + url?: string; + }; +} + +interface AssertSomeResult { + result: boolean; + actual: { id: string; count: number }; + expected: { id: string; count: number }; + message: string; +} +interface AssertNoneResult { + result: boolean; + actual: FoundWarning[]; + expected: FoundWarning[]; + message: string; +} + +/** + * Returns a qunit assert result object which passes if the given warning + * `id` was found *exactly* `count` times. + * + * Fails if not found or found more or less than `count`. + * Fails if `until` not specified + * Optionally fails if `until` has been passed. + */ +function verifyWarning(config: WarningConfig, label?: string): AssertSomeResult { + // TODO optionally throw if `until` is the current version or older than current version + let matchedWarnings = WARNINGS_FOR_TEST.filter((warning) => { + let isMatched = warning.options.id === config.id; + if (!isMatched && config.message) { + // TODO when we hit this we should throw an error in the near future + isMatched = checkMatcher(warning.message, config.message); + } + return isMatched; + }); + WARNINGS_FOR_TEST = WARNINGS_FOR_TEST.filter((warning) => { + !matchedWarnings.includes(warning); + }); + HANDLED_WARNINGS_FOR_TEST.push(...matchedWarnings); + + let expectedCount = typeof config.count === 'number' ? config.count : 1; + let passed = matchedWarnings.length === expectedCount; + + return { + result: passed, + actual: { id: config.id, count: matchedWarnings.length }, + expected: { id: config.id, count: expectedCount }, + message: + label || + `Expected ${expectedCount} warning${expectedCount === 1 ? '' : 's'} for ${config.id} during test, ${ + passed ? expectedCount : 'but ' + matchedWarnings.length + } warnings were found.`, + }; +} + +function verifyNoWarning(label?: string): AssertNoneResult { + const UNHANDLED_WARNINGS = WARNINGS_FOR_TEST; + WARNINGS_FOR_TEST = []; + + let warningStr = UNHANDLED_WARNINGS.reduce((a, b) => { + return `${a}${b.message}\n`; + }, ''); + + let passed = UNHANDLED_WARNINGS.length === 0; + + return { + result: passed, + actual: UNHANDLED_WARNINGS, + expected: [], + message: + label || + `Expected 0 warnings during test, ${ + passed ? '0' : 'but ' + UNHANDLED_WARNINGS.length + } warnings were found.\n${warningStr}`, + }; +} + +export function configureWarningHandler(assert: Assert) { + if (HAS_REGISTERED !== true) { + registerWarnHandler(function (message, options /*, next*/) { + if (WARNINGS_FOR_TEST && options) { + WARNINGS_FOR_TEST.push({ message, options }); + } + // we do not call next to avoid spamming the console + }); + HAS_REGISTERED = true; + } + + WARNINGS_FOR_TEST = []; + HANDLED_WARNINGS_FOR_TEST = []; + + assert.expectWarning = expectWarning; + assert.expectNoWarning = expectNoWarning; +} + +async function expectWarning( + this: Assert, + cb: () => unknown, + config: string | RegExp | WarningConfig, + label?: string +): Promise { + let origWarnings = WARNINGS_FOR_TEST; + let callback: (() => unknown) | null = null; + + if (typeof cb !== 'function') { + config = cb; + callback = null; + } else { + callback = cb; + } + + if (typeof config === 'string' || config instanceof RegExp) { + config = { + id: 'unknown-data-warning', + count: 1, + message: config, + until: '4.0', + }; + } + + if (callback) { + WARNINGS_FOR_TEST = []; + let result = callback(); + if (isThenable(result)) { + await result; + } + } + + let result = verifyWarning(config, label); + + if (!DEBUG) { + result = { + result: true, + actual: { id: config.id, count: 0 }, + expected: { id: config.id, count: 0 }, + message: `Warnings do not trigger in production environments`, + }; + } + + this.pushResult(result); + WARNINGS_FOR_TEST = origWarnings.concat(WARNINGS_FOR_TEST); +} + +async function expectNoWarning(this: Assert, cb: () => void | Promise, label?: string) { + let origWarnings = WARNINGS_FOR_TEST; + + if (cb) { + WARNINGS_FOR_TEST = []; + let result = cb(); + if (isThenable(result)) { + await result; + } + } + + let result = verifyNoWarning(label); + + if (!DEBUG) { + result = { + result: true, + actual: [], + expected: [], + message: `Warnings do not trigger in production environments`, + }; + } + + this.pushResult(result); + WARNINGS_FOR_TEST = origWarnings.concat(WARNINGS_FOR_TEST); +} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/check-matcher.ts b/packages/unpublished-test-infra/src/test-support/asserts/check-matcher.ts similarity index 75% rename from packages/unpublished-test-infra/addon-test-support/qunit-asserts/check-matcher.ts rename to packages/unpublished-test-infra/src/test-support/asserts/check-matcher.ts index 0a32e455482..072b2333f23 100644 --- a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/check-matcher.ts +++ b/packages/unpublished-test-infra/src/test-support/asserts/check-matcher.ts @@ -1,8 +1,8 @@ -function includes(message, search) { - return message.includes ? message.includes(search) : message.indexOf(search) !== -1; +function includes(message: string, search: string) { + return message.includes ? message.includes(search) : message.includes(search); } -export function checkMatcher(message, matcher) { +export function checkMatcher(message: string, matcher: string | RegExp) { if (typeof matcher === 'string') { return includes(message, matcher); } else if (matcher instanceof RegExp) { diff --git a/packages/unpublished-test-infra/src/test-support/asserts/index.ts b/packages/unpublished-test-infra/src/test-support/asserts/index.ts new file mode 100644 index 00000000000..379d0efe45e --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/asserts/index.ts @@ -0,0 +1,130 @@ +import { TestContext } from '@ember/test-helpers'; + +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Diagnostic } from '@warp-drive/diagnostic/-types'; + +import type Assert from 'ember-data-qunit-asserts'; +import type { CacheOperation, NotificationType } from '@ember-data/store'; +import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier'; + +import { configureAssertAllDeprecations } from './assert-all-deprecations'; +import { configureAssertionHandler } from './assert-assertion'; +import { configureBetterAsserts } from './assert-better'; +import { configureDeprecationHandler, DeprecationConfig, FoundDeprecation } from './assert-deprecation'; +import { configureNotificationsAssert } from './assert-notification'; +import { configureWarningHandler, WarningConfig } from './assert-warning'; + +declare module '@warp-drive/diagnostic' { + export interface EmberDiagnostic extends Diagnostic { + expectDeprecation(options: DeprecationConfig, label?: string): void; + expectDeprecation( + callback: () => void | Promise, + options: DeprecationConfig | string | RegExp, + label?: string + ): Promise; + expectNoDeprecation( + callback: () => void | Promise, + label?: string, + filter?: (deprecation: FoundDeprecation) => boolean + ): Promise; + expectWarning(callback: () => unknown, options: WarningConfig | string | RegExp): Promise; + expectNoWarning(callback: () => unknown): Promise; + expectAssertion(callback: () => unknown, matcher: string | RegExp): Promise; + expectNoAssertion(callback: () => unknown): Promise; + watchNotifications(store?: unknown): void; + /** + * Asserts that each member of actual strictly matches the corresponding member of expected. + * Asserts that actual is an array and has the same length as expected. + */ + arrayStrictEquals(actual: unknown, expected: T[], message: string): void; + /** + * Asserts that the given identifier has been notified of a change to the given bucket + * and optional key the given number of times during the test. + * + * Clears the notification count for the given identifier, bucket and key after the assertion + * is made so that it is easy to assert notification counts in between steps of a test. + */ + notified( + identifier: StableDocumentIdentifier | StableRecordIdentifier, + bucket: NotificationType | CacheOperation, + key: string | null, + count: number + ): void; + + clearNotifications(): void; + } + + export interface EmberHooks { + onSuiteStart: (cb: () => void | Promise) => void; + onSuiteFinish: (cb: () => void | Promise) => void; + beforeModule: (cb: () => void | Promise) => void; + afterModule: (cb: () => void | Promise) => void; + beforeEach: (cb: (this: TC, assert: EmberDiagnostic) => void | Promise) => void; + afterEach: (cb: (this: TC, assert: EmberDiagnostic) => void | Promise) => void; + } + + export function module( + name: string, + callback: (hooks: EmberHooks) => void | Promise + ): void; + + export function skip( + name: string, + callback: (this: TC, assert: EmberDiagnostic) => void | Promise + ): void; + + export function todo( + name: string, + callback: (this: TC, assert: EmberDiagnostic) => void | Promise + ): void; + + export function test( + name: string, + callback: (this: TC, assert: EmberDiagnostic) => void | Promise + ): void; +} + +type CompatAssert = Assert & { + test: { + expected: number; + }; +}; + +export interface ExpandedHooks { + onSuiteStart: (cb: () => void | Promise) => void; + onSuiteFinish: (cb: () => void | Promise) => void; + beforeModule: (cb: () => void | Promise) => void; + afterModule: (cb: () => void | Promise) => void; + beforeEach: (cb: (assert: CompatAssert) => void | Promise) => void; + afterEach: (cb: (assert: CompatAssert) => void | Promise) => void; +} + +function upgradeHooks(hooks: NestedHooks): ExpandedHooks { + const upgraded = hooks as unknown as ExpandedHooks; + // eslint-disable-next-line no-restricted-globals + const Framework = typeof QUnit !== 'undefined' ? QUnit : null; + if (Framework) { + // eslint-disable-next-line @typescript-eslint/unbound-method + upgraded.onSuiteStart = Framework.begin; + // eslint-disable-next-line @typescript-eslint/unbound-method + upgraded.onSuiteFinish = Framework.done; + // eslint-disable-next-line @typescript-eslint/unbound-method + upgraded.beforeModule = hooks.before; + // eslint-disable-next-line @typescript-eslint/unbound-method + upgraded.afterModule = hooks.after; + } + return upgraded; +} + +export default function configureAsserts(hooks: NestedHooks) { + const upgraded = upgradeHooks(hooks); + + upgraded.beforeEach(function (this: TestContext, assert) { + configureAssertionHandler(assert); + configureDeprecationHandler(assert); + configureWarningHandler(assert); + configureBetterAsserts(assert); + configureNotificationsAssert.call(this, assert); + }); + configureAssertAllDeprecations(upgraded); +} diff --git a/packages/unpublished-test-infra/addon-test-support/qunit-asserts/utils/is-thenable.ts b/packages/unpublished-test-infra/src/test-support/asserts/utils/is-thenable.ts similarity index 100% rename from packages/unpublished-test-infra/addon-test-support/qunit-asserts/utils/is-thenable.ts rename to packages/unpublished-test-infra/src/test-support/asserts/utils/is-thenable.ts diff --git a/packages/unpublished-test-infra/src/test-support/deprecated-test.ts b/packages/unpublished-test-infra/src/test-support/deprecated-test.ts new file mode 100644 index 00000000000..0787b5d56cf --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/deprecated-test.ts @@ -0,0 +1,5 @@ +import { skip, test } from 'qunit'; + +import { createDeprecatedTestFn } from './test'; + +export const deprecatedTest = createDeprecatedTestFn({ skip, test }); diff --git a/packages/unpublished-test-infra/src/test-support/test-helpers.ts b/packages/unpublished-test-infra/src/test-support/test-helpers.ts new file mode 100644 index 00000000000..4d482ce47b9 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/test-helpers.ts @@ -0,0 +1,18 @@ +import { getTestMetadata, setupContext, SetupContextOptions, teardownContext, TestContext } from '@ember/test-helpers'; + +import type { EmberHooks } from '@warp-drive/diagnostic'; + +export function setupTest(hooks: EmberHooks, opts?: SetupContextOptions) { + const options = { waitForSettled: false, ...opts }; + + hooks.beforeEach(async function () { + let testMetadata = getTestMetadata(this); + testMetadata.framework = 'qunit'; + + await setupContext(this, options); + }); + + hooks.afterEach(function (this: TestContext) { + return teardownContext(this, options); + }); +} diff --git a/packages/unpublished-test-infra/src/test-support/test-in-debug.ts b/packages/unpublished-test-infra/src/test-support/test-in-debug.ts new file mode 100644 index 00000000000..5c809037f4e --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/test-in-debug.ts @@ -0,0 +1,15 @@ +import { type TestContext } from '@ember/test-helpers'; + +import { skip, test as qunitTest } from 'qunit'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +export function test(label: string, callback: (this: TestContext, assert: Assert) => void | Promise): void { + if (DEBUG) { + qunitTest(`[DEBUG-ONLY] ${label}`, callback); + } else { + skip(`[DEBUG-ONLY] ${label}`, callback); + } +} + +export default test; diff --git a/packages/unpublished-test-infra/src/test-support/test.ts b/packages/unpublished-test-infra/src/test-support/test.ts new file mode 100644 index 00000000000..0930eb59201 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/test.ts @@ -0,0 +1,92 @@ +import { type TestContext } from '@ember/test-helpers'; + +import { DEBUG } from '@warp-drive/build-config/env'; +import VERSION, { COMPAT_VERSION } from './version'; +import { DeprecationConfig } from './asserts/assert-deprecation'; + +// small comparison function for major and minor semver values +function gte(EDVersion: string, DeprecationVersion: string): boolean { + let _edv = EDVersion.split('.'); + let _depv = DeprecationVersion.split('.'); + // compare major + let major = +_edv[0] >= +_depv[0]; + // compare minor + let minor = +_edv[1] >= +_depv[1]; + return major || minor; +} + +type LimitedAssert = { + ok: (value: unknown, message?: string) => void; + expectDeprecation(options: DeprecationConfig, label?: string): void; + expectDeprecation( + callback: () => void | Promise, + options: DeprecationConfig | string | RegExp, + label?: string + ): Promise; +}; + +export function createDeprecatedTestFn(run: { + skip: (name: string, cb: (this: TC, assert: T) => void | Promise) => void; + test: (name: string, cb: (this: TC, assert: T) => void | Promise) => void; +}) { + const { skip, test } = run; + return function deprecatedTest( + testName: string, + deprecation: { + until: `${number}.${number}`; + id: string; + count: number; + // this test should only run in debug mode + debugOnly?: boolean; + // this test should remain in the codebase but + // should be refactored to no longer use the deprecated feature + refactor?: boolean; + }, + testCallback: (this: TC, assert: T) => void | Promise + ) { + // '4.0' + if (typeof deprecation.until !== 'string' || deprecation.until.length < 3) { + throw new Error(`deprecatedTest expects { until } to be a version.`); + } + // 'ds.' + if (typeof deprecation.id !== 'string' || deprecation.id.length < 8) { + throw new Error(`deprecatedTest expects { id } to be a meaningful string`); + } + + async function interceptor(this: TC, assert: T) { + await testCallback.call(this, assert); + if (DEBUG) { + // @ts-expect-error test is not typed correctly + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (typeof assert.test.expected === 'number') { + // @ts-expect-error test is not typed correctly + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + assert.test.expected += 1; + } + assert.expectDeprecation(deprecation); + } + } + + let testFn = test; + if (COMPAT_VERSION && gte(COMPAT_VERSION, VERSION)) { + testFn = skip; + } + if (!DEBUG) { + if (deprecation.debugOnly) { + testFn = skip; + } + } + + if (gte(VERSION, deprecation.until)) { + testFn(`DEPRECATION ${deprecation.id} until ${deprecation.until} | ${testName}`, interceptor); + } else { + testFn(`DEPRECATION ${deprecation.id} until ${deprecation.until} | ${testName}`, function (assert) { + if (deprecation.refactor === true) { + assert.ok(false, 'This test includes use of a deprecated feature that should now be refactored.'); + } else { + assert.ok(false, 'This test is for a deprecated feature whose time has come and should be removed'); + } + }); + } + }; +} diff --git a/packages/unpublished-test-infra/addon-test-support/todo.js b/packages/unpublished-test-infra/src/test-support/todo.js similarity index 98% rename from packages/unpublished-test-infra/addon-test-support/todo.js rename to packages/unpublished-test-infra/src/test-support/todo.js index 51ceecbe7ac..17103d33664 100644 --- a/packages/unpublished-test-infra/addon-test-support/todo.js +++ b/packages/unpublished-test-infra/src/test-support/todo.js @@ -1,6 +1,6 @@ import QUnit, { skip, test } from 'qunit'; -import { DEBUG } from '@ember-data/env'; +import { DEBUG } from '@warp-drive/build-config/env'; export default function todo(description, callback) { if (DEBUG) { diff --git a/packages/unpublished-test-infra/src/test-support/version.ts b/packages/unpublished-test-infra/src/test-support/version.ts new file mode 100644 index 00000000000..af063f65390 --- /dev/null +++ b/packages/unpublished-test-infra/src/test-support/version.ts @@ -0,0 +1,12 @@ +import { getGlobalConfig, getOwnConfig } from '@embroider/macros'; + +type OWNCONFIG = { + VERSION: string; +}; + +const VERSION: string = getOwnConfig().VERSION; +const COMPAT_VERSION: string = getGlobalConfig<{ WarpDrive: { compatWith: string } }>().WarpDrive.compatWith; + +export default VERSION; + +export { COMPAT_VERSION }; diff --git a/packages/unpublished-test-infra/src/testem/custom-dot-reporter.js b/packages/unpublished-test-infra/src/testem/custom-dot-reporter.js deleted file mode 100644 index 523283670c5..00000000000 --- a/packages/unpublished-test-infra/src/testem/custom-dot-reporter.js +++ /dev/null @@ -1,46 +0,0 @@ -let DotReporter = require('testem/lib/reporters/dot_reporter'); - -class CustomDotReporter extends DotReporter { - allData = []; - - finish() { - const data = this.allData; - let totalDuration = 0; - data.sort((a, b) => { - return a.runDuration > b.runDuration ? -1 : 1; - }); - - this.out.write(`\n\n50 Longest Running Tests\n`); - for (let i = 0; i < data.length; i++) { - const { name, runDuration } = data[i]; - - if (i < 50) { - this.out.write(`\n\t${i + 1}.\t${runDuration}ms\t${name}`); - } - totalDuration += runDuration; - } - this.out.write(`\n\n\tAvg Duration of all ${data.length} tests: ${Math.round(totalDuration / data.length)}ms\n\n`); - - super.finish(); - - if (process.env.ASSERT_ALL_DEPRECATIONS === 'true') { - this.out.write('\n============ Deprecations ============\n'); - this.out.write(JSON.stringify(this.deprecations, null, 2) + '\n'); - this.out.write('======================================\n'); - } - } - - report(prefix, data) { - super.report(prefix, data); - data.runDuration = data.runDuration || 0; - this.allData.push(data); - } - - reportMetadata(tag, metadata) { - if (tag === 'deprecations') { - this.deprecations = metadata; - } - } -} - -module.exports = CustomDotReporter; diff --git a/packages/unpublished-test-infra/testem.js b/packages/unpublished-test-infra/testem.js deleted file mode 100644 index 165c63b837d..00000000000 --- a/packages/unpublished-test-infra/testem.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - launch_in_ci: ['Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - // --no-sandbox is needed when running Chrome inside a container - process.env.CI ? '--no-sandbox' : null, - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - ].filter(Boolean), - }, - }, -}; diff --git a/packages/unpublished-test-infra/testem/custom-dot-reporter.js b/packages/unpublished-test-infra/testem/custom-dot-reporter.js new file mode 100644 index 00000000000..ef93f3e64c2 --- /dev/null +++ b/packages/unpublished-test-infra/testem/custom-dot-reporter.js @@ -0,0 +1,470 @@ +/* eslint-disable no-console */ +/* eslint-disable no-magic-numbers */ +const fs = require('node:fs'); +const path = require('node:path'); + +const DotReporter = require('testem/lib/reporters/dot_reporter'); +const chalk = require('chalk'); + +const SLOW_TEST_COUNT = 50; +const JSON_INDENT = 2; +const DEFAULT_TIMEOUT = 8_000; +const TIMEOUT_BUFFER = 0; +const DEFAULT_TEST_TIMEOUT = 21_000; + +function indent(text, width = 2) { + return text + .split('\n') + .map((line) => { + return new Array(width).join('\t') + line; + }) + .join('\n'); +} + +class CustomDotReporter extends DotReporter { + allData = []; + failedTests = []; + hasMemoryData = false; + partitions = {}; + partitionsMap = {}; + _running = {}; + totalLines = 0; + lineFailures = []; + + reportFinalMemoryStats() { + this.out.write( + `\n\n\t==================================\n\tBrowser Memory Usage Stats\n\t==================================\n` + ); + this.out.write(`\tBrowser\t\t| Tests\t| Delta\t\t| {startMetrics}\t\t| {endMetrics}\t\t\t|\n`); + const keys = Object.keys(this.partitions); + keys.sort(); + keys.forEach((key) => { + const value = this.partitions[key]; + + const start = value.find((v) => v.originalResultObj?.memoryUsage); + const end = value.findLast((v) => v.originalResultObj?.memoryUsage); + const a = end?.originalResultObj.memoryUsage.usedJSHeapSize || 0; + const b = start?.originalResultObj.memoryUsage.usedJSHeapSize || 0; + + const delta = Math.round(((a - b) / 1024 / 1024) * 100) / 100; + this.out.write( + `\t${key}\t| ${value.length}\t| ${delta}\t| ${formatBytes( + start?.originalResultObj.memoryUsage || 0 + )}\t| ${formatBytes(end?.originalResultObj.memoryUsage || 0)}|\n` + ); + }); + this.out.write(`\t==================================\n\n`); + + keys.forEach((key) => { + this.out.write(`\n\n`); + const value = this.partitions[key]; + value.forEach((data) => { + if (!data.originalResultObj?.memoryUsage) { + if (!data.skipped && !data.todo) { + this.out.write(`${data.runDuration}ms MemoryUNK ${data.name}\n`); + } + } else { + this.out.write(`${data.runDuration}ms ${data.name}\n`); + } + }); + this.out.write(`\n\n`); + }); + + this.out.write(`\n\n`); + } + + reportPendingTests() { + const getBrowserId = (v) => { + return this.partitionsMap[v.launcherId] || v.launcherId; + }; + + const values = Object.values(this._running).sort((a, b) => { + return getBrowserId(a) > getBrowserId(b) ? -1 : 1; + }); + if (values.length) { + this.out.write(chalk.red(`\n\nStill Pending Tests:\n\n`)); + values.forEach((v) => { + this.out.write(chalk.yellow(`\t⛔️ Stuck: ${getBrowserId(v)} #${v.id} - ${chalk.white(v.name)}\n`)); + }); + } + } + + reportSlowTests() { + const data = this.allData; + let totalDuration = 0; + let testsToPrint = SLOW_TEST_COUNT; + data.sort((a, b) => { + return a.runDuration > b.runDuration ? -1 : 1; + }); + + this.out.write( + `\n\n======================================\n\t${chalk.yellow( + `${data.length < SLOW_TEST_COUNT ? data.length : SLOW_TEST_COUNT} Longest Running Tests` + )}\n======================================\n` + ); + for (let i = 0; i < data.length; i++) { + const { name, runDuration } = data[i]; + + if (i < testsToPrint) { + // this test is a known offender + if (runDuration > DEFAULT_TIMEOUT + TIMEOUT_BUFFER) { + this.out.write(`\n\t${i + 1}.\t[S] ${chalk.yellow(runDuration.toLocaleString('en-US') + 'ms')}\t${name}`); + testsToPrint++; + } else { + this.out.write(`\n\t${i + 1}.\t${chalk.yellow(runDuration.toLocaleString('en-US') + 'ms')}\t${name}`); + } + } + totalDuration += runDuration; + } + this.out.write( + chalk.yellow(`\n\n\tAvg Duration of all ${data.length} tests: ${Math.round(totalDuration / data.length)}ms\n\n`) + ); + } + + reportFailedTests() { + this.failedTests.forEach((failure) => printFailure(this.out, failure)); + const failedTestIds = []; + let allFailuresAccounted = true; + this.failedTests.forEach((failure) => { + if (failure.originalResultObj?.testId && !failure.error?.message?.includes('No tests matched the testId')) { + failedTestIds.push(failure.originalResultObj.testId); + } else { + allFailuresAccounted = false; + } + }); + + const cacheFile = path.join(__dirname, '../failed-test-log.txt'); + if (allFailuresAccounted) { + if (failedTestIds.length) { + fs.writeFileSync(cacheFile, failedTestIds.join(','), { encoding: 'utf-8' }); + this.out.write( + chalk.yellow( + `\n\nSaved ${chalk.white(failedTestIds.length)} Failed Tests for Retry with IDS ${chalk.white( + failedTestIds.join(',') + )} in ${chalk.grey(cacheFile)}` + ) + ); + this.out.write( + `\n\nTo run failed tests locally, visit http://localhost:7357/tests/index.html?${failedTestIds + .map((id) => `testId=${id}`) + .join('&')}` + ); + } else { + remove(cacheFile); + } + } else { + this.out.write( + chalk.red(`\n\n⚠️ Unable to save failed tests for retry, not all failures had test IDs, cleaning up`) + ); + remove(cacheFile); + } + } + + reportDeprecations() { + if (process.env.ASSERT_ALL_DEPRECATIONS === 'true') { + this.out.write('\n============ Deprecations ============\n'); + this.out.write(JSON.stringify(this.deprecations, null, JSON_INDENT) + '\n'); + this.out.write('======================================\n'); + } + } + + /** + * Runs on Test Suite Completion + */ + finish() { + this.endTime = new Date(); + + if (this.hasMemoryData) { + this.reportFinalMemoryStats(); + } + + if (this.failedTests.length) { + this.out.write( + chalk.red( + `\n\n${this.failedTests.length} Tests Failed. Complete stack traces for failures will print at the end.` + ) + ); + } + + this.reportPendingTests(); + + this.out.write(`\n\n\n----------\n\n\n`); + + this.reportSlowTests(); + this.reportFailedTests(); + + this.out.write('\n\n'); + this.out.write(this.summaryDisplay()); + this.out.write('\n\n'); + + this.reportDeprecations(); + } + + displayFullResult(prefix, result) { + if (this.silent) { + return; + } + if (result.passed && !result.todo) { + this.out.write( + `\t✅ ${chalk.green('Passed')}: ${chalk.white(`#${result.__testNo}`)} ${chalk.grey( + result.runDuration.toLocaleString('en-US') + 'ms' + )} ${result.name}\n` + ); + } else if (!result.passed && result.todo) { + this.out.write( + chalk.cyan( + `\t🛠️ TODO: ${chalk.white(`#${result.__testNo}`)} ${chalk.grey( + result.runDuration.toLocaleString('en-US') + 'ms' + )} ${result.name}\n` + ) + ); + } else if (result.skipped) { + this.out.write( + chalk.yellow( + `\t⚠️ Skipped: ${chalk.white(`#${result.__testNo}`)} ${chalk.grey( + result.runDuration.toLocaleString('en-US') + 'ms' + )} ${result.name}\n` + ) + ); + } else { + this.out.write( + chalk.red( + `\t💥 Failed: ${chalk.white(`#${result.__testNo}`)} ${chalk.grey( + result.runDuration.toLocaleString('en-US') + 'ms' + )} ${result.name}\n` + ) + ); + this.out.write( + `\t\topen test locally: http://localhost:7357/tests/index.html?testId=${result.originalResultObj?.testId}\n` + ); + } + } + + display(prefix, result) { + if (this.silent) { + return; + } + if (this.currentLineChars > this.maxLineChars) { + this.totalLines++; + this.currentLineChars = 0; + const lineFailures = this.lineFailures; + this.lineFailures = []; + + if (lineFailures.length) { + this.out.write('\n\n'); + lineFailures.forEach((failure) => { + this.displayFullResult(null, failure); + }); + } + + if (this.totalLines % 5 === 0) { + this.out.write(`\n${chalk.magenta((this.totalLines * this.maxLineChars).toLocaleString('en-US'))}⎡\t`); + } else { + this.out.write('\n\t'); + } + } + if (result.passed && !result.todo) { + this.out.write(chalk.grey('.')); + } else if (!result.passed && result.todo) { + this.out.write(chalk.cyan('T')); + } else if (result.skipped) { + this.out.write(chalk.yellow('*')); + } else { + this.out.write(chalk.bold(chalk.red('F'))); + } + this.currentLineChars += 1; + } + + /** + * Runs on Individual Test Completion + */ + report(prefix, data) { + data.__testNo = this.allData.length + 1; + data.launcher = prefix; + data.runDuration = data.runDuration || 0; + if (data.launcherId && this._running[data.launcherId]?.id === data.originalResultObj?.id) { + delete this._running[data.launcherId]; + } + + addTestMetaToName(this.partitions, this.partitionsMap, data); + + if (data.originalResultObj?.memoryUsage) { + this.hasMemoryData = true; + } + + if (process.env.DISPLAY_TEST_NAMES) { + this.displayFullResult(prefix, data); + } else { + if (this.allData.length === 0) { + this.displayDotLegend(); + } + this.display(prefix, data); + } + + this.total++; + if (data.skipped) { + this.skipped++; + } else if (data.passed && !data.todo) { + this.pass++; + } else if (!data.passed && data.todo) { + this.todo++; + } + + this.allData.push(data); + if (data.failed && !data.skipped && !data.todo) { + this.lineFailures.push(data); + this.failedTests.push(data); + } + } + + displayDotLegend() { + this.out.write('\n\tLegend\n\t========='); + this.out.write(chalk.green('\n\tPass:\t.')); + this.out.write(chalk.cyan('\n\tTodo:\tT')); + this.out.write(chalk.yellow('\n\tSkip:\t*')); + this.out.write(chalk.bold(chalk.red('\n\tFail:\tF'))); + this.out.write('\n\n\t'); + } + + /** + * runs on individual test start + * only because we patch testem to emit this. + * Normally it will not alert us to test start even + * though it knows. + */ + testStarted(_browserName, data) { + data._testStarted = Date.now(); + this._running[data.launcherId] = data; + this.ensureTimeoutCheck(); + } + + /** + * Periodically checks for hung tests and reports them + */ + ensureTimeoutCheck() { + if (this._timeoutId) { + return; + } + this._timeoutId = setTimeout(() => { + Object.keys(this._running).forEach((key) => { + const data = this._running[key]; + const duration = Date.now() - data._testStarted; + if (duration > DEFAULT_TEST_TIMEOUT) { + this.out.write( + chalk.grey( + `\n\n⚠️ ${chalk.yellow('Pending:')} ${chalk.white(data.name)} has been running for ${chalk.yellow( + duration.toLocaleString('en-US') + 'ms' + )}, this is likely a bug.\n` + ) + ); + } + }); + this._timeoutId = null; + if (Object.keys(this._running).length) { + this.ensureTimeoutCheck(); + } + }, DEFAULT_TEST_TIMEOUT / 3); + } + + reportMetadata(tag, metadata) { + if (tag === 'deprecations') { + this.deprecations = metadata; + } + } + + summaryDisplay() { + const lines = [chalk.yellow(`[duration - ${this.duration()} ms]`), summaryDisplay(this)]; + return lines.join('\n'); + } +} + +function formatBytes({ jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize, granular }) { + if (granular) { + console.log({ granular }); + throw new Error(`Granular Memory Data Access Detected! (This is Awesome, Alert @runspired)`); + } + const used = Math.round((usedJSHeapSize / 1024 / 1024) * 100) / 100; + const total = Math.round((totalJSHeapSize / 1024 / 1024) * 100) / 100; + const max = Math.round((jsHeapSizeLimit / 1024 / 1024) * 100) / 100; + return `{${used}/${total}/${max} Mb} `; +} + +function addTestMetaToName(partitions, partitionsMap, result) { + if (!result.originalResultObj && result.error?.message !== 'Received SIGINT signal') { + if (result.logs?.[0]?.text.includes('Browser failed to connect')) { + result.logs.forEach((log) => { + if (typeof log?.text === 'string') { + log.text.split('\n').forEach((line) => { + console.log(line); + }); + } else { + console.log(log); + } + }); + } + } + const bytes = + !result.skipped && !result.todo && result.originalResultObj?.memoryUsage + ? formatBytes(result.originalResultObj.memoryUsage) + : ''; + //const index = result.name.indexOf('-'); + const remainder = result.name; //result.name.substring(index); + const start = ''; //result.name.substring(0, index - 1); + result.name = `${bytes}${start} #${result.originalResultObj?.id || '??'} ${remainder}`; + partitions[start] = partitions[start] || []; + partitionsMap[start] = result.launcherId; + partitionsMap[result.launcherId] = start; + partitions[start].push(result); +} + +function printFailure(out, result) { + console.log(JSON.stringify(result, null, 2)); + out.write(chalk.red(`\n\t💥 Failed: ${result.runDuration.toLocaleString('en-US')}ms ${result.name}\n`)); + const error = result.error; + + if (!error) { + // we aren't totally sure what to do in these situations yet + // so lets not be lossy around the info that might be helpful :) + console.log(result); + return; + } + + if (error.message) { + out.write(`\t\t${error.message}\n`); + } + + if ('expected' in error && 'actual' in error) { + out.write(`\n\t\texpected: ${error.negative ? 'NOT ' : ''}${error.expected}\n\t\tactual: ${error.actual}\n`); + } + + if (error.stack) { + out.write(`\n${indent(error.stack)}`); + } + + out.write('\n\n'); +} + +// Instead of completely removing, we replace the contents with an empty string so that CI will still cache it. +// While this shouldn't ever really be necessary it's a bit more correct to make sure that the log gets cleared +// in the cache as well. +function remove(filePath) { + fs.writeFileSync(filePath, '', { encoding: 'utf-8' }); +} + +function summaryDisplay(reporter) { + const lines = [ + 'Total ' + reporter.total, + chalk.green('# pass ' + reporter.pass), + chalk.yellow('# skip ' + reporter.skipped), + chalk.cyan('# todo ' + reporter.todo), + chalk.red('# fail ' + (reporter.total - reporter.pass - reporter.skipped - reporter.todo)), + ]; + + if (this.pass + this.skipped + this.todo === this.total) { + lines.push(''); + lines.push('# ok'); + } + return lines.join('\n'); +} + +module.exports = CustomDotReporter; diff --git a/packages/unpublished-test-infra/testem/testem.js b/packages/unpublished-test-infra/testem/testem.js new file mode 100644 index 00000000000..efe20b75cf3 --- /dev/null +++ b/packages/unpublished-test-infra/testem/testem.js @@ -0,0 +1,152 @@ +/* eslint-disable n/no-unpublished-require */ +/* eslint-disable no-console */ +const fs = require('node:fs'); +const path = require('node:path'); + +const TestemReporter = require('./custom-dot-reporter'); + +let TEST_FAILURES; +try { + const filePath = path.join(__dirname, '../failed-test-log.txt'); + TEST_FAILURES = fs.readFileSync(filePath, { encoding: 'utf-8' }); +} catch { + TEST_FAILURES = false; +} +const FAILURES = TEST_FAILURES ? TEST_FAILURES.trim().split(',') : false; +if (FAILURES) { + console.log(`Retrying ${FAILURES.length} failed tests: ${FAILURES.join(',')}`); +} + +console.log( + `\n\nLaunching with ${process.env.CI_BROWSER || 'Chrome'} (worker count ${ + process.env.EMBER_EXAM_SPLIT_COUNT || process.env.EXAM_PARALLEL_COUNT || 1 + })\n\n` +); +const TEST_PAGE_FLAGS = [ + 'hidepassed', + 'nocontainer', + process.env.DEBUG_MEMORY ? 'debugMemory' : false, + process.env.CI || process.env.DEBUG_MEMORY ? 'disableHtmlReporter' : false, + process.env.DEBUG_PERFORMANCE ? 'debugPerformance' : false, + process.env.GC_BREATHE_TIME ? `gcBreatheTime=${process.env.GC_BREATHE_TIME}` : false, + FAILURES ? `testId=${FAILURES.join('&testId=')}` : false, +].filter(Boolean); + +// default 10min per-browser test suite run timeout in seconds +const DEFAULT_BROWSER_TIMEOUT = 600; +// when using a configured timeout we adjust it down a bit to account for +// to make sure we cleanup before external things cleanup +const BROWSER_TIMEOUT_BUFFER = 30; +const BROWSER_TIMEOUT = process.env.BROWSER_TIMEOUT + ? Number(process.env.BROWSER_TIMEOUT) - BROWSER_TIMEOUT_BUFFER + : DEFAULT_BROWSER_TIMEOUT; + +module.exports = { + framework: 'qunit', + test_page: `tests/index.html?${TEST_PAGE_FLAGS.join('&')}`, + disable_watching: true, + launch_in_ci: [process.env.CI_BROWSER || 'Chrome'], + launch_in_dev: ['Chrome'], + tap_quiet_logs: true, + // timeout for the total suite run in this browser in seconds + timeout: BROWSER_TIMEOUT, + + // these may help debug CI in some situations + // debug: true, + // chrome_stderr_info_only: true, + reporter: TestemReporter, + parallel: process.env.EMBER_EXAM_SPLIT_COUNT || process.env.EXAM_PARALLEL_COUNT, + browser_disconnect_timeout: 45, + browser_start_timeout: 45, + client_decycle_depth: 10, + socket_heartbeat_timeout: 75, // test timeout is 60s, so this needs to be longer + browser_reconnect_limit: 10, + // See https://github.com/testem/testem/issues/1021#issuecomment-1186607152 + socket_server_options: { + maxHttpBufferSize: 10e7, + }, + browser_args: { + Chrome: { + ci: [ + '--headless=new', + '--no-sandbox', + + // this may help debug CI in some situations + '--enable-logging=stderr', + '--v=1', + + // when debugging memory usage this gives us better data + process.env.DEBUG_MEMORY ? '--enable-precise-memory-info' : false, + process.env.DEBUG_MEMORY ? '--js-flags="--allow-natives-syntax --expose-gc"' : false, + + // these prevent user account + // and extensions from mucking with things + '--incognito', + '--bwsi', + + // On Ubuntu this dev-shm-usage speeds you up on bigger machines + // and slows you down on smaller. We are on a bigger CI box now. + // '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-extensions', + '--disable-translate', + '--disable-3d-apis', + '--disable-software-rasterizer', + '--disable-webgl', + // '--disable-web-security', + '--disable-remote-fonts', + '--blink-settings=imagesEnabled=false', + '--mute-audio', + + // ubuntu-16-core seems to be unhappy with this being set to a non-zero port + // throws: ERROR:socket_posix.cc(147)] bind() failed: Address already in use (98) + '--remote-debugging-port=0', + '--remote-debugging-address=0.0.0.0', + '--window-size=1440,900', + '--no-proxy-server', + '--proxy-bypass-list=*', + "--proxy-server='direct://'", + ].filter(Boolean), + dev: [ + '--headless=new', + '--no-sandbox', + + // this may help debug CI in some situations + '--enable-logging=stderr', + '--v=1', + + // when debugging memory usage this gives us better data + process.env.DEBUG_MEMORY ? '--enable-precise-memory-info' : false, + process.env.DEBUG_MEMORY ? '--js-flags="--allow-natives-syntax --expose-gc"' : false, + + // these prevent user account + // and extensions from mucking with things + '--incognito', + '--bwsi', + + // On Ubuntu this dev-shm-usage speeds you up on bigger machines + // and slows you down on smaller. We are on a bigger CI box now. + // '--disable-dev-shm-usage', + '--disable-gpu', + '--disable-extensions', + '--disable-translate', + '--disable-3d-apis', + '--disable-software-rasterizer', + '--disable-webgl', + // '--disable-web-security', + '--disable-remote-fonts', + '--blink-settings=imagesEnabled=false', + '--mute-audio', + + // ubuntu-16-core seems to be unhappy with this being set to a non-zero port + // throws: ERROR:socket_posix.cc(147)] bind() failed: Address already in use (98) + '--remote-debugging-port=0', + '--remote-debugging-address=0.0.0.0', + '--window-size=1440,900', + '--no-proxy-server', + '--proxy-bypass-list=*', + "--proxy-server='direct://'", + ], + }, + }, +}; diff --git a/packages/unpublished-test-infra/tests/dummy/app/app.js b/packages/unpublished-test-infra/tests/dummy/app/app.js deleted file mode 100644 index 65b2f588b56..00000000000 --- a/packages/unpublished-test-infra/tests/dummy/app/app.js +++ /dev/null @@ -1,14 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -export default class App extends Application { - modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; - Resolver = Resolver; -} - -loadInitializers(App, config.modulePrefix); diff --git a/packages/unpublished-test-infra/tests/dummy/app/index.html b/packages/unpublished-test-infra/tests/dummy/app/index.html deleted file mode 100644 index 61400b20f56..00000000000 --- a/packages/unpublished-test-infra/tests/dummy/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Dummy - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/packages/unpublished-test-infra/tests/dummy/app/router.js b/packages/unpublished-test-infra/tests/dummy/app/router.js deleted file mode 100644 index bb6e77cc4af..00000000000 --- a/packages/unpublished-test-infra/tests/dummy/app/router.js +++ /dev/null @@ -1,10 +0,0 @@ -import EmberRouter from '@ember/routing/router'; - -import config from './config/environment'; - -export default class Router extends EmberRouter { - location = config.locationType; - rootURL = config.rootURL; -} - -Router.map(function () {}); diff --git a/packages/unpublished-test-infra/tests/dummy/app/routes/application/template.hbs b/packages/unpublished-test-infra/tests/dummy/app/routes/application/template.hbs deleted file mode 100644 index 1c967ea89c3..00000000000 --- a/packages/unpublished-test-infra/tests/dummy/app/routes/application/template.hbs +++ /dev/null @@ -1,6 +0,0 @@ - -{{outlet}} diff --git a/packages/unpublished-test-infra/tests/dummy/config/environment.js b/packages/unpublished-test-infra/tests/dummy/config/environment.js deleted file mode 100644 index 50a22cd35b0..00000000000 --- a/packages/unpublished-test-infra/tests/dummy/config/environment.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'dummy', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - compatWith: process.env.COMPAT_WITH, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/packages/unpublished-test-infra/tests/index.html b/packages/unpublished-test-infra/tests/index.html deleted file mode 100644 index 7eceb491196..00000000000 --- a/packages/unpublished-test-infra/tests/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - Dummy Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/packages/unpublished-test-infra/tests/test-helper.js b/packages/unpublished-test-infra/tests/test-helper.js deleted file mode 100644 index 63ce2b9da19..00000000000 --- a/packages/unpublished-test-infra/tests/test-helper.js +++ /dev/null @@ -1,16 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); - -setApplication(Application.create(config.APP)); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/packages/unpublished-test-infra/tests/unit/deprecations-stripped-test.js b/packages/unpublished-test-infra/tests/unit/deprecations-stripped-test.js deleted file mode 100644 index cdab5bda853..00000000000 --- a/packages/unpublished-test-infra/tests/unit/deprecations-stripped-test.js +++ /dev/null @@ -1,24 +0,0 @@ -import config from 'dummy/config/environment'; -import { module, test } from 'qunit'; - -import { DEPRECATE_3_12 } from '@ember-data/deprecations'; - -const { compatWith } = config; - -module('test compatWith', function () { - test('deprecation strips', function (assert) { - let deprecation_stripped = true; - - if (DEPRECATE_3_12) { - deprecation_stripped = false; - } - - if (compatWith === '3.0' || compatWith === '3.8') { - assert.false(deprecation_stripped, 'deprecation code was not stripped'); - } else if (compatWith === '3.12' || compatWith === '3.16' || compatWith === '99.0') { - assert.true(deprecation_stripped, 'deprecation code was stripped'); - } else { - // do nothing - } - }); -}); diff --git a/packages/unpublished-test-infra/tsconfig.json b/packages/unpublished-test-infra/tsconfig.json new file mode 100644 index 00000000000..33655ba29d2 --- /dev/null +++ b/packages/unpublished-test-infra/tsconfig.json @@ -0,0 +1,73 @@ +{ + "include": ["src/addon-test-support/**/*", "src/**/*", "tests/**/*", "../../@types/ember-data-qunit-asserts"], + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "emitDeclarationOnly": true, + "noEmitOnError": false, + "noImplicitOverride": false, + // Enable faster builds + // but causes us to not rebuild properly + "composite": true, + "incremental": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "unstable-preview-types", + "inlineSourceMap": true, + "inlineSources": true, + "types": ["ember-source/types"], + "paths": { + "ember-data-qunit-asserts": ["../../@types/ember-data-qunit-asserts"], + "@ember-data/request": ["../request/unstable-preview-types"], + "@ember-data/request/*": ["../request/unstable-preview-types/*"], + "@ember-data/store": ["../store/unstable-preview-types"], + "@ember-data/store/*": ["../store/unstable-preview-types/*"], + "@ember-data/tracking": ["../tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../tracking/unstable-preview-types/*"], + "@warp-drive/build-config": ["../build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../build-config/unstable-preview-types/*"], + "@warp-drive/core-types": ["../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"], + "@warp-drive/diagnostic": ["../diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../diagnostic/unstable-preview-types/*"], + "@ember-data/request-utils": ["../request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../diagnostic" + }, + { + "path": "../build-config" + }, + { + "path": "../store" + }, + { + "path": "../request" + }, + { + "path": "../core-types" + }, + { + "path": "../tracking" + }, + { + "path": "../request-utils" + } + ] +} diff --git a/packages/unpublished-test-infra/vite.config.mjs b/packages/unpublished-test-infra/vite.config.mjs new file mode 100644 index 00000000000..7a2bfa665b2 --- /dev/null +++ b/packages/unpublished-test-infra/vite.config.mjs @@ -0,0 +1,18 @@ +import { createConfig } from '@warp-drive/internal-config/vite/config.js'; + +export const externals = [ + 'semver', + '@ember/test-helpers', + '@ember/version', + '@ember/debug', + 'ember-data-qunit-asserts', +]; +export const entryPoints = ['./src/test-support/**/*.ts', './src/test-support/**/*.js']; + +export default createConfig( + { + entryPoints, + externals, + }, + import.meta.resolve +); diff --git a/patches/@ember__test-helpers@3.3.0.patch b/patches/@ember__test-helpers@3.3.0.patch new file mode 100644 index 00000000000..40b5588be36 --- /dev/null +++ b/patches/@ember__test-helpers@3.3.0.patch @@ -0,0 +1,115 @@ +diff --git a/addon-test-support/@ember/test-helpers/build-owner.js b/addon-test-support/@ember/test-helpers/build-owner.js +index f5581db0b593755de4aac0741a75ca947325e152..d7a978bd5e9aa42b26a0181b50958a0dc903a9c6 100644 +--- a/addon-test-support/@ember/test-helpers/build-owner.js ++++ b/addon-test-support/@ember/test-helpers/build-owner.js +@@ -18,13 +18,17 @@ import legacyBuildRegistry from './-internal/build-registry'; + @param {Ember.Resolver} [resolver] the resolver to use to back a "mock owner" + @returns {Promise} a promise resolving to the generated "owner" + */ +-export default function buildOwner(application, resolver) { ++export default function buildOwner(application, resolver, options) { + if (application) { + // @ts-ignore: this type is correct and will check against Ember 4.12 or 5.1 + // or later. However, the first round of preview types in Ember 4.8 does not + // include the `visit` API (it was missing for many years!) and therefore + // there is no way to make this assignable accross all supported versions. +- return application.boot().then(app => app.buildInstance().boot()); ++ const appBoot = application.boot(); ++ return appBoot.then(app => { ++ const instance = app.buildInstance(options); ++ return instance.boot(options); ++ }); + } + if (!resolver) { + throw new Error('You must set up the ember-test-helpers environment with either `setResolver` or `setApplication` before running any tests.'); +diff --git a/addon-test-support/@ember/test-helpers/build-owner.ts b/addon-test-support/@ember/test-helpers/build-owner.ts +index 4123927cf9953a31dc6f2a476a3919a3511c4301..e9869859c612cd88e2b3ab5f1d2dbbdbbfc3e00e 100644 +--- a/addon-test-support/@ember/test-helpers/build-owner.ts ++++ b/addon-test-support/@ember/test-helpers/build-owner.ts +@@ -46,7 +46,7 @@ export default function buildOwner( + // or later. However, the first round of preview types in Ember 4.8 does not + // include the `visit` API (it was missing for many years!) and therefore + // there is no way to make this assignable accross all supported versions. +- return application.boot().then((app) => app.buildInstance().boot()); ++ return application.boot(options).then((app) => app.buildInstance().boot()); + } + + if (!resolver) { +diff --git a/addon-test-support/@ember/test-helpers/index.js b/addon-test-support/@ember/test-helpers/index.js +index 51b66ee9c663de864efa3822db874e7f317a0b31..13d50d4dad24406999985bc699cf0dd92e371120 100644 +--- a/addon-test-support/@ember/test-helpers/index.js ++++ b/addon-test-support/@ember/test-helpers/index.js +@@ -3,7 +3,7 @@ export { getApplication, setApplication } from './application'; + export { default as hasEmberVersion } from './has-ember-version'; + export { default as setupContext, getContext, setContext, unsetContext, pauseTest, resumeTest, getDeprecations, getDeprecationsDuringCallback, getWarnings, getWarningsDuringCallback } from './setup-context'; + export { default as teardownContext } from './teardown-context'; +-export { default as setupRenderingContext, render, clearRender } from './setup-rendering-context'; ++export { default as setupRenderingContext, hasCalledSetupRenderingContext, render, clearRender } from './setup-rendering-context'; + export { default as rerender } from './rerender'; + export { default as setupApplicationContext, visit, currentRouteName, currentURL } from './setup-application-context'; + export { default as settled, isSettled, getSettledState } from './settled'; +diff --git a/addon-test-support/@ember/test-helpers/setup-context.js b/addon-test-support/@ember/test-helpers/setup-context.js +index c68ef11c92225725c8e8eb2b824c787c5644ec8b..84ae96cb4c9aa89d480698e20212df7b6d8331ef 100644 +--- a/addon-test-support/@ember/test-helpers/setup-context.js ++++ b/addon-test-support/@ember/test-helpers/setup-context.js +@@ -319,7 +319,8 @@ export default function setupContext(base, options = {}) { + return; + }).then(() => { + let { +- resolver ++ resolver, ++ rootElement + } = options; + + // This handles precedence, specifying a specific option of +@@ -329,9 +330,9 @@ export default function setupContext(base, options = {}) { + // At some later time this can be extended to support specifying a custom + // engine or application... + if (resolver) { +- return buildOwner(null, resolver); ++ return buildOwner(null, resolver, { rootElement }); + } +- return buildOwner(getApplication(), getResolver()); ++ return buildOwner(getApplication(), getResolver(), { rootElement }); + }).then(owner => { + associateDestroyableChild(context, owner); + Object.defineProperty(context, 'owner', { +diff --git a/addon-test-support/@ember/test-helpers/setup-rendering-context.js b/addon-test-support/@ember/test-helpers/setup-rendering-context.js +index fe5b40b8d2eff73de9c5b5ad18f8c0c70e264b6b..b821daa5929dcc6c05a51f7c98185fd0af9b549e 100644 +--- a/addon-test-support/@ember/test-helpers/setup-rendering-context.js ++++ b/addon-test-support/@ember/test-helpers/setup-rendering-context.js +@@ -15,7 +15,8 @@ import { ComponentRenderMap, SetUsage } from './setup-context'; + const OUTLET_TEMPLATE = hbs`{{outlet}}`; + const EMPTY_TEMPLATE = hbs``; + const INVOKE_PROVIDED_COMPONENT = hbs``; +-const hasCalledSetupRenderingContext = Symbol(); ++export const hasCalledSetupRenderingContext = Symbol('hasCalledSetupRenderingContext'); ++ + // Isolates the notion of transforming a TextContext into a RenderingTestContext. + // eslint-disable-next-line require-jsdoc + function prepare(context) { +diff --git a/dist-types/index.d.ts b/dist-types/index.d.ts +index fe2bd64a876f41b9d270e49ca8c8d53b5d3000b6..54a5b34130063c22f1bb70bc02a0569892415e8b 100644 +--- a/dist-types/index.d.ts ++++ b/dist-types/index.d.ts +@@ -6,7 +6,7 @@ export type { BaseContext, DeprecationFailure, TestContext, Warning, SetupContex + export { default as setupContext, getContext, setContext, unsetContext, pauseTest, resumeTest, getDeprecations, getDeprecationsDuringCallback, getWarnings, getWarningsDuringCallback, } from './setup-context'; + export { default as teardownContext } from './teardown-context'; + export type { TeardownContextOptions } from './teardown-context'; +-export { default as setupRenderingContext, render, clearRender, } from './setup-rendering-context'; ++export { default as setupRenderingContext, render, clearRender, hasCalledSetupRenderingContext } from './setup-rendering-context'; + export type { RenderingTestContext } from './setup-rendering-context'; + export { default as rerender } from './rerender'; + export { default as setupApplicationContext, visit, currentRouteName, currentURL, } from './setup-application-context'; +diff --git a/dist-types/setup-rendering-context.d.ts b/dist-types/setup-rendering-context.d.ts +index bc748d27e22905a061a9ab683255bbf0172c0c62..f7d0e81b838ecb62718312aaff15b078a6d22ebe 100644 +--- a/dist-types/setup-rendering-context.d.ts ++++ b/dist-types/setup-rendering-context.d.ts +@@ -1,6 +1,6 @@ + import { BaseContext, TestContext } from './setup-context'; + import { Owner } from './build-owner'; +-declare const hasCalledSetupRenderingContext: unique symbol; ++export declare const hasCalledSetupRenderingContext: unique symbol; + export interface RenderingTestContext extends TestContext { + element: Element | Document; + [hasCalledSetupRenderingContext]?: true; \ No newline at end of file diff --git a/patches/@ember__test-helpers@4.0.4.patch b/patches/@ember__test-helpers@4.0.4.patch new file mode 100644 index 00000000000..98b903a858f --- /dev/null +++ b/patches/@ember__test-helpers@4.0.4.patch @@ -0,0 +1,101 @@ +diff --git a/declarations/index.d.ts b/declarations/index.d.ts +index 954e71657c36d820ea4127322d5d1ad051499549..e08cfa594727d300932a4407105cfaf5c49f84c8 100644 +--- a/declarations/index.d.ts ++++ b/declarations/index.d.ts +@@ -6,7 +6,7 @@ export type { BaseContext, DeprecationFailure, TestContext, Warning, SetupContex + export { default as setupContext, getContext, setContext, unsetContext, pauseTest, resumeTest, getDeprecations, getDeprecationsDuringCallback, getWarnings, getWarningsDuringCallback, } from './setup-context.ts'; + export { default as teardownContext } from './teardown-context.ts'; + export type { TeardownContextOptions } from './teardown-context.ts'; +-export { default as setupRenderingContext, render, clearRender, } from './setup-rendering-context.ts'; ++export { default as setupRenderingContext, render, clearRender, hasCalledSetupRenderingContext } from './setup-rendering-context.ts'; + export type { RenderingTestContext } from './setup-rendering-context.ts'; + export { default as rerender } from './rerender.ts'; + export { default as setupApplicationContext, visit, currentRouteName, currentURL, } from './setup-application-context.ts'; +diff --git a/declarations/setup-rendering-context.d.ts b/declarations/setup-rendering-context.d.ts +index 933ae9046751ce93e5a65e484df1d28961157fd2..313434746489d3f6aca00165ac8963f281f8bfc8 100644 +--- a/declarations/setup-rendering-context.d.ts ++++ b/declarations/setup-rendering-context.d.ts +@@ -1,6 +1,6 @@ + import { type BaseContext, type TestContext } from './setup-context.ts'; + import type { Owner } from './build-owner.ts'; +-declare const hasCalledSetupRenderingContext: unique symbol; ++export declare const hasCalledSetupRenderingContext: unique symbol; + export interface RenderingTestContext extends TestContext { + element: Element | Document; + [hasCalledSetupRenderingContext]?: true; +diff --git a/dist/build-owner.js b/dist/build-owner.js +index b19c6aa9c1b03b3c858b403164169d6c65205a21..4152a03a24b93f19e17f596d0439673b1aa33d64 100644 +--- a/dist/build-owner.js ++++ b/dist/build-owner.js +@@ -20,13 +20,17 @@ import buildRegistry from './-internal/build-registry.js'; + @param {Ember.Resolver} [resolver] the resolver to use to back a "mock owner" + @returns {Promise} a promise resolving to the generated "owner" + */ +-function buildOwner(application, resolver) { ++function buildOwner(application, resolver, options) { + if (application) { + // @ts-ignore: this type is correct and will check against Ember 4.12 or 5.1 + // or later. However, the first round of preview types in Ember 4.8 does not + // include the `visit` API (it was missing for many years!) and therefore + // there is no way to make this assignable accross all supported versions. +- return application.boot().then(app => app.buildInstance().boot()); ++ const appBoot = application.boot(); ++ return appBoot.then(app => { ++ const instance = app.buildInstance(options); ++ return instance.boot(options); ++ }); + } + if (!resolver) { + throw new Error('You must set up the ember-test-helpers environment with either `setResolver` or `setApplication` before running any tests.'); +diff --git a/dist/index.js b/dist/index.js +index 9ee37706860074f1496ad97bcc374569f97d4f32..565f11c54abd583f55c3c7514ae94b3ee5c139f7 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -3,7 +3,7 @@ export { getApplication, setApplication } from './application.js'; + export { default as hasEmberVersion } from './has-ember-version.js'; + export { j as currentRouteName, k as currentURL, g as getContext, c as getDeprecations, d as getDeprecationsDuringCallback, m as getSettledState, e as getWarnings, f as getWarningsDuringCallback, l as isSettled, p as pauseTest, o as resetOnerror, r as resumeTest, b as setContext, s as settled, h as setupApplicationContext, a as setupContext, n as setupOnerror, u as unsetContext, v as visit } from './setup-context-Cx9HkMuO.js'; + export { default as teardownContext } from './teardown-context.js'; +-export { clearRender, render, default as setupRenderingContext } from './setup-rendering-context.js'; ++export { clearRender, render, default as setupRenderingContext, hasCalledSetupRenderingContext } from './setup-rendering-context.js'; + export { default as rerender } from './rerender.js'; + export { default as waitUntil } from './wait-until.js'; + export { default as validateErrorHandler } from './validate-error-handler.js'; +diff --git a/dist/setup-context-Cx9HkMuO.js b/dist/setup-context-Cx9HkMuO.js +index c510d8ef811229be05561ff71d78639bd03f6d9d..2bba9ef08c4efcedad947ac54a326c123e296b14 100644 +--- a/dist/setup-context-Cx9HkMuO.js ++++ b/dist/setup-context-Cx9HkMuO.js +@@ -809,7 +809,8 @@ function setupContext(base, options = {}) { + return; + }).then(() => { + const { +- resolver ++ resolver, ++ rootElement + } = options; + + // This handles precedence, specifying a specific option of +@@ -819,9 +820,9 @@ function setupContext(base, options = {}) { + // At some later time this can be extended to support specifying a custom + // engine or application... + if (resolver) { +- return buildOwner(null, resolver); ++ return buildOwner(null, resolver, { rootElement }); + } +- return buildOwner(getApplication(), getResolver()); ++ return buildOwner(getApplication(), getResolver(), { rootElement }); + }).then(owner => { + associateDestroyableChild(context, owner); + Object.defineProperty(context, 'owner', { +diff --git a/dist/setup-rendering-context.js b/dist/setup-rendering-context.js +index e1dd64849799e7ad579ab88ee208c9d9a3cafbe5..9291e951275d6204d8deb273bbe7f86a67bcfb96 100644 +--- a/dist/setup-rendering-context.js ++++ b/dist/setup-rendering-context.js +@@ -19,7 +19,7 @@ const EMPTY_TEMPLATE = precompileTemplate("", { + const INVOKE_PROVIDED_COMPONENT = precompileTemplate("", { + strictMode: false + }); +-const hasCalledSetupRenderingContext = Symbol(); ++export const hasCalledSetupRenderingContext = Symbol('hasCalledSetupRenderingContext'); + // Isolates the notion of transforming a TextContext into a RenderingTestContext. + // eslint-disable-next-line require-jsdoc + function prepare(context) { diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 00000000000..e5a0d6d1572 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,9 @@ +# Patch Overview + +## @ember/test-helpers @3.3.0 + +This patch exists because @ember/test-helpers 4.0+ does not support ember-source 3.28. +We install 3.3.0 during our ember-try scenario, and make pnpm happy by installing 3.3.0 all +the time in the root dev-dependencies. + +It should be the same as the 4.x patch. diff --git a/patches/qunit@2.19.4.patch b/patches/qunit@2.19.4.patch new file mode 100644 index 00000000000..856162c3c6d --- /dev/null +++ b/patches/qunit@2.19.4.patch @@ -0,0 +1,73 @@ +diff --git a/qunit/qunit.js b/qunit/qunit.js +index 5e48b79303e8bfe33ba60a9407c2d67879a07841..649cd30f59991f1870570d6ec831ed602862a772 100644 +--- a/qunit/qunit.js ++++ b/qunit/qunit.js +@@ -8,6 +8,10 @@ + */ + (function () { + 'use strict'; ++ function getKeys(obj) { ++ if (!obj) { return []; } ++ return Object.keys(obj).concat(getKeys(Object.getPrototypeOf(obj))); ++ } + + function _typeof(obj) { + "@babel/helpers - typeof"; +@@ -1003,10 +1007,7 @@ + return '[object Object]'; + } + dump.up(); +- var keys = []; +- for (var key in map) { +- keys.push(key); +- } ++ var keys = getKeys(map); + + // Some properties are not always enumerable on Error objects. + var nonEnumerableProperties = ['message', 'name']; +@@ -5647,6 +5648,9 @@ + appendToolbar(beginDetails); + } + function appendTest(name, testId, moduleName) { ++ if (window$1.DISABLE_QUNIT_HTML_REPORTER) { ++ return; ++ } + var tests = id('qunit-tests'); + if (!tests) { + return; +@@ -5831,6 +5835,13 @@ + assertList.appendChild(assertLi); + }); + QUnit.testDone(function (details) { ++ // This test passed if it has no unexpected failed assertions ++ var testPassed = details.failed > 0 ? details.todo : !details.todo; ++ if (!testPassed) { ++ stats.failedTests.push(details.testId); ++ } ++ stats.completed++; ++ + var tests = id('qunit-tests'); + var testItem = id('qunit-test-output-' + details.testId); + if (!tests || !testItem) { +@@ -5849,13 +5860,10 @@ + var good = details.passed; + var bad = details.failed; + +- // This test passed if it has no unexpected failed assertions +- var testPassed = details.failed > 0 ? details.todo : !details.todo; + if (testPassed) { + // Collapse the passing tests + addClass(assertList, 'qunit-collapsed'); + } else { +- stats.failedTests.push(details.testId); + if (config.collapse) { + if (!collapseNext) { + // Skip collapsing the first failing test +@@ -5871,7 +5879,6 @@ + var testTitle = testItem.firstChild; + var testCounts = bad ? "" + bad + ', ' + "" + good + ', ' : ''; + testTitle.innerHTML += " (" + testCounts + details.assertions.length + ')'; +- stats.completed++; + if (details.skipped) { + testItem.className = 'skipped'; + var skipped = document.createElement('em'); \ No newline at end of file diff --git a/patches/testem@3.11.0.patch b/patches/testem@3.11.0.patch new file mode 100644 index 00000000000..33f9d42d38b --- /dev/null +++ b/patches/testem@3.11.0.patch @@ -0,0 +1,44 @@ +diff --git a/lib/runners/browser_test_runner.js b/lib/runners/browser_test_runner.js +index 7c79e7349a9636b97ef6fd349d14d73f8bc55976..475fabca395829beb35a8127c470cbfa9cd750dd 100644 +--- a/lib/runners/browser_test_runner.js ++++ b/lib/runners/browser_test_runner.js +@@ -142,6 +142,7 @@ module.exports = class BrowserTestRunner { + } + + let tap = new BrowserTapConsumer(socket); ++ tap.on('tests-start', this.onTestsStart.bind(this)); + tap.on('test-result', this.onTestResult.bind(this)); + tap.on('all-test-results', this.onAllTestResults.bind(this)); + tap.on('all-test-results', () => { +@@ -165,8 +166,12 @@ module.exports = class BrowserTestRunner { + + onTestsStart(testData) { + if (testData) { ++ Object.assign(testData, { ++ launcherId: this.launcherId ++ }); + this.currentTestContext = testData; + this.currentTestContext.state = 'executing'; ++ this.reporter.testStarted(this.launcher.name, testData); + } + } + +diff --git a/lib/utils/reporter.js b/lib/utils/reporter.js +index 3b619b8b840fa0b5b3c1ad45f48c3df65b030359..e46d9489c7bf51b150e99b2986e5156eb36998f2 100644 +--- a/lib/utils/reporter.js ++++ b/lib/utils/reporter.js +@@ -67,6 +67,14 @@ class Reporter { + } + } + ++ testStarted(name, data) { ++ this.reporters.forEach(reporter => { ++ if (reporter.testStarted) { ++ reporter.testStarted(name, data); ++ } ++ }); ++ } ++ + close() { + this.finish(); + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74ce3dbf2e8..91e24f010d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,126 +1,80 @@ -lockfileVersion: '9.0' +lockfileVersion: '6.0' settings: autoInstallPeers: false excludeLinksFromLockfile: false overrides: - ember-auto-import: ^2.6.1 - '@embroider/macros': 1.10.0 + ember-auto-import: ^2.10.0 broccoli-funnel: ^3.0.8 broccoli-merge-trees: ^4.2.0 - ember-cli-babel: ^7.26.11 - ember-cli-htmlbars: ^6.2.0 + '@glimmer/validator': ^0.92.3 + '@glint/core': 1.5.0 + '@glint/environment-ember-loose': 1.5.0 + '@glint/environment-ember-template-imports': 1.5.0 + '@glint/template': 1.5.0 + ember-cli-babel: ^8.2.0 + ember-cli-htmlbars: ^6.3.0 + ember-cli-typescript: ^5.3.0 + webpack: 5.94.0 + qunit: 2.19.4 + ember-compatibility-helpers: ^1.2.7 + testem: ~3.11.0 + +packageExtensionsChecksum: 10ad8371ed129520d984fb4ace777d13 + +patchedDependencies: + '@ember/test-helpers@3.3.0': + hash: gppmtiox6pymwamrfimkbxfrsm + path: patches/@ember__test-helpers@3.3.0.patch + '@ember/test-helpers@4.0.4': + hash: zignhd6n3rugkiuawsmbuxfdka + path: patches/@ember__test-helpers@4.0.4.patch + qunit@2.19.4: + hash: 2jwk2nz4gqke2k5hv6ptj42llu + path: patches/qunit@2.19.4.patch + testem@3.11.0: + hash: yfkum5c5nfihh3ce3f64tnp5rq + path: patches/testem@3.11.0.patch importers: .: + dependencies: + turbo: + specifier: ^1.13.4 + version: 1.13.4 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/eslint-parser': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4)(eslint@8.37.0) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember/edition-utils': - specifier: ^1.2.0 - version: 1.2.0 - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^7.24.5 + version: 7.26.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 3.3.0 + version: 3.3.0(patch_hash=gppmtiox6pymwamrfimkbxfrsm)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/env': - specifier: ^0.1.7 - version: 0.1.7 - '@types/ember': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember-qunit': - specifier: ^5.0.2 - version: 5.0.2(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@types/ember-resolver': - specifier: ^5.0.13 - version: 5.0.13(@babel/core@7.21.4) - '@types/ember-testing-helpers': - specifier: ^0.0.4 - version: 0.0.4 - '@types/ember__application': - specifier: ^4.0.5 - version: 4.0.5(@babel/core@7.21.4) - '@types/ember__array': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember__component': - specifier: ^4.0.12 - version: 4.0.12(@babel/core@7.21.4) - '@types/ember__controller': - specifier: ^4.0.4 - version: 4.0.4(@babel/core@7.21.4) - '@types/ember__debug': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember__engine': - specifier: ^4.0.4 - version: 4.0.4(@babel/core@7.21.4) - '@types/ember__error': - specifier: ^4.0.2 - version: 4.0.2 - '@types/ember__object': - specifier: ^4.0.5 - version: 4.0.5(@babel/core@7.21.4) - '@types/ember__polyfills': - specifier: ^4.0.1 - version: 4.0.1 - '@types/ember__routing': - specifier: ^4.0.12 - version: 4.0.12(@babel/core@7.21.4) - '@types/ember__runloop': - specifier: ^4.0.2 - version: 4.0.2(@babel/core@7.21.4) - '@types/ember__service': - specifier: ^4.0.2 - version: 4.0.2(@babel/core@7.21.4) - '@types/ember__string': - specifier: ^3.0.10 - version: 3.0.10 - '@types/ember__template': - specifier: ^4.0.1 - version: 4.0.1 - '@types/ember__test': - specifier: ^4.0.1 - version: 4.0.1(@babel/core@7.21.4) - '@types/ember__utils': - specifier: ^4.0.2 - version: 4.0.2(@babel/core@7.21.4) - '@types/htmlbars-inline-precompile': - specifier: ^3.0.0 - version: 3.0.0 - '@types/qunit': - specifier: ^2.19.4 - version: 2.19.4 - '@types/rsvp': - specifier: ^4.0.4 - version: 4.0.4 - '@typescript-eslint/eslint-plugin': - specifier: ^5.57.1 - version: 5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint@8.37.0)(typescript@5.0.3) - '@typescript-eslint/parser': - specifier: ^5.57.1 - version: 5.57.1(eslint@8.37.0)(typescript@5.0.3) + version: 1.1.2(@babel/core@7.26.0) + '@glint/core': + specifier: 1.5.0 + version: 1.5.0(typescript@5.6.3) + '@glint/environment-ember-loose': + specifier: 1.5.0 + version: 1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0) + '@glint/environment-ember-template-imports': + specifier: 1.5.0 + version: 1.5.0(@glint/environment-ember-loose@1.5.0)(@glint/template@1.5.0) + '@glint/template': + specifier: 1.5.0 + version: 1.5.0 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + badge-maker: + specifier: 4.1.0 + version: 4.1.0 + bun-types: + specifier: ^1.1.30 + version: 1.1.34 chalk: specifier: ^4.1.2 version: 4.1.2 @@ -130,96 +84,51 @@ importers: command-line-args: specifier: ^5.2.1 version: 5.2.1 + comment-json: + specifier: ^4.2.5 + version: 4.2.5 common-tags: specifier: ^1.8.2 version: 1.8.2 debug: - specifier: ^4.3.4 - version: 4.3.4 - ember-cli: - specifier: ~4.11.0 - version: 4.11.0(debug@4.3.4) + specifier: ^4.3.7 + version: 4.3.7(supports-color@8.1.1) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - eslint: - specifier: ^8.37.0 - version: 8.37.0 - eslint-config-prettier: - specifier: ^8.8.0 - version: 8.8.0(eslint@8.37.0) - eslint-plugin-ember: - specifier: ^11.4.9 - version: 11.4.9(eslint@8.37.0) - eslint-plugin-ember-data: - specifier: link:./packages/unpublished-eslint-rules - version: link:packages/unpublished-eslint-rules - eslint-plugin-import: - specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint@8.37.0) - eslint-plugin-mocha: - specifier: ^10.1.0 - version: 10.1.0(eslint@8.37.0) - eslint-plugin-node: - specifier: ^11.1.0 - version: 11.1.0(eslint@8.37.0) - eslint-plugin-prettier: - specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.8.0(eslint@8.37.0))(eslint@8.37.0)(prettier@2.8.7) - eslint-plugin-qunit: - specifier: ^7.3.4 - version: 7.3.4(eslint@8.37.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.37.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) execa: - specifier: ^5.1.1 - version: 5.1.1 - fromentries: - specifier: ^1.3.2 - version: 1.3.2 - git-repo-info: - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^9.4.1 + version: 9.5.1 git-repo-version: specifier: ^1.0.2 version: 1.0.2 - glob: - specifier: ^9.3.4 - version: 9.3.4 - json-typescript: - specifier: ^1.1.2 - version: 1.1.2 + globby: + specifier: ^14.0.2 + version: 14.0.2 lerna-changelog: specifier: ^2.2.0 version: 2.2.0 - loader.js: - specifier: ^4.7.0 - version: 4.7.0 prettier: - specifier: ^2.8.7 - version: 2.8.7 + specifier: ^3.3.2 + version: 3.3.3 + prettier-plugin-ember-template-tag: + specifier: ^2.0.2 + version: 2.0.4(prettier@3.3.3) rimraf: - specifier: ^4.4.1 - version: 4.4.1 + specifier: ^5.0.6 + version: 5.0.10 semver: - specifier: ^7.3.8 - version: 7.3.8 + specifier: ^7.6.3 + version: 7.6.3 silent-error: specifier: ^1.1.1 version: 1.1.1 - testem: - specifier: ^3.10.1 - version: 3.10.1(debug@4.3.4) typescript: - specifier: ~5.0.3 - version: 5.0.3 + specifier: ^5.4.5 + version: 5.6.3 url: - specifier: ^0.11.0 - version: 0.11.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^0.11.4 + version: 0.11.4 yuidocjs: specifier: ^0.10.2 version: 0.10.2 @@ -227,81 +136,171 @@ importers: specifier: 1.0.5 version: 1.0.5 + config: + dependencies: + '@babel/cli': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/eslint-parser': + specifier: 7.25.8 + version: 7.25.8(@babel/core@7.26.0)(eslint@9.14.0) + '@embroider/addon-dev': + specifier: ^4.3.1 + version: 4.3.1(rollup@4.25.0) + '@eslint/js': + specifier: ^9.13.0 + version: 9.14.0 + '@rollup/plugin-babel': + specifier: ^6.0.4 + version: 6.0.4(@babel/core@7.26.0)(rollup@4.25.0) + '@typescript-eslint/eslint-plugin': + specifier: ^8.10.0 + version: 8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.10.0 + version: 8.14.0(eslint@9.14.0)(typescript@5.6.3) + ember-eslint-parser: + specifier: ^0.5.2 + version: 0.5.3(@babel/core@7.26.0)(@typescript-eslint/parser@8.14.0)(eslint@9.14.0) + eslint: + specifier: ^9.12.0 + version: 9.14.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.14.0) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0) + eslint-plugin-mocha: + specifier: ^10.5.0 + version: 10.5.0(eslint@9.14.0) + eslint-plugin-n: + specifier: ^17.11.0 + version: 17.13.1(eslint@9.14.0) + eslint-plugin-qunit: + specifier: ^8.1.2 + version: 8.1.2(eslint@9.14.0) + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@9.14.0) + globals: + specifier: ^15.11.0 + version: 15.12.0 + rollup: + specifier: ^4.17.2 + version: 4.25.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + typescript-eslint: + specifier: ^8.10.0 + version: 8.14.0(eslint@9.14.0)(typescript@5.6.3) + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) + vite-plugin-dts: + specifier: ^3.9.1 + version: 3.9.1(rollup@4.25.0)(typescript@5.6.3)(vite@5.4.11) + packages/-ember-data: dependencies: '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/debug': - specifier: workspace:4.12.8 - version: file:packages/debug(@ember-data/store@4.12.8)(@ember/string@4.0.0)(webpack@5.77.0) + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/graph': - specifier: workspace:4.12.8 - version: file:packages/graph(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/json-api': - specifier: workspace:4.12.8 - version: file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../request + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - '@glimmer/env': - specifier: ^0.1.7 - version: 0.1.7 - broccoli-merge-trees: - specifier: ^4.2.0 - version: 4.2.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 + '@types/qunit': + specifier: 2.19.10 + version: 2.19.10 + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + eslint: + specifier: ^9.12.0 + version: 9.14.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: '@ember-data/adapter': injected: true @@ -315,1783 +314,1370 @@ importers: injected: true '@ember-data/model': injected: true - '@ember-data/private-build-infra': - injected: true '@ember-data/request': injected: true + '@ember-data/request-utils': + injected: true '@ember-data/serializer': injected: true '@ember-data/store': injected: true '@ember-data/tracking': injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true - packages/adapter: + packages/active-record: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-test-info: - specifier: ^1.0.0 - version: 1.0.0 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) - '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/request': + injected: true + '@ember-data/request-utils': injected: true - - packages/debug: - dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember/edition-utils': - specifier: ^1.2.0 - version: 1.2.0 - '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - devDependencies: '@ember-data/store': - specifier: workspace:4.12.8 - version: link:../store - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 - dependenciesMeta: - '@ember-data/private-build-infra': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - packages/graph: + packages/adapter: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@ember/edition-utils': - specifier: ^1.2.0 + specifier: 1.2.0 version: 1.2.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + ember-cli-test-info: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) - '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) + version: 1.1.2(@babel/core@7.26.0) + '@types/jquery': + specifier: ^3.5.30 + version: 3.5.32 + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + decorator-transforms: + specifier: ^2.2.2 + version: 2.3.0(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/legacy-compat': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - packages/json-api: + packages/build-config: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember/edition-utils': - specifier: ^1.2.0 - version: 1.2.0 + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + babel-import-util: + specifier: ^2.1.1 + version: 2.1.1 + broccoli-funnel: + specifier: ^3.0.8 + version: 3.0.8 + semver: + specifier: ^7.6.3 + version: 7.6.3 devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) - '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) - '@glimmer/component': - specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) - ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/node': + specifier: ^20.14.2 + version: 20.17.6 + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + bun-types: + specifier: ^1.1.30 + version: 1.1.34 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 - dependenciesMeta: - '@ember-data/graph': - injected: true - '@ember-data/private-build-infra': - injected: true + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) - packages/legacy-compat: + packages/core-types: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) - '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) - '@types/ember__string': - specifier: ^3.0.15 - version: 3.0.15 - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': - injected: true - '@ember/string': + '@warp-drive/build-config': injected: true - packages/model: + packages/debug: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cached-decorator-polyfill: - specifier: ^1.0.1 - version: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-string-utils: - specifier: ^1.1.0 - version: 1.1.0 - ember-cli-test-info: - specifier: ^1.0.0 - version: 1.0.0 - inflection: - specifier: ~2.0.1 - version: 2.0.1 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + decorator-transforms: + specifier: ^2.2.2 + version: 2.3.0(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - packages/private-build-infra: + packages/diagnostic: dependencies: - '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-transform-block-scoping': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember/edition-utils': - specifier: ^1.2.0 - version: 1.2.0 - '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - babel-import-util: - specifier: ^1.3.0 - version: 1.3.0 - babel-plugin-debug-macros: - specifier: ^0.3.4 - version: 0.3.4(@babel/core@7.21.4) - babel-plugin-filter-imports: - specifier: ^4.0.0 - version: 4.0.0 - babel6-plugin-strip-class-callcheck: - specifier: ^6.0.0 - version: 6.0.0 - broccoli-debug: - specifier: ^0.6.5 - version: 0.6.5 - broccoli-file-creator: - specifier: ^2.1.1 - version: 2.1.1 - broccoli-funnel: - specifier: ^3.0.8 - version: 3.0.8 - broccoli-merge-trees: - specifier: ^4.2.0 - version: 4.2.0 - broccoli-rollup: - specifier: ^5.0.0 - version: 5.0.0 - calculate-cache-key-for-tree: - specifier: ^2.0.0 - version: 2.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) chalk: - specifier: ^4.1.2 - version: 4.1.2 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-path-utils: - specifier: ^1.0.0 - version: 1.0.0 - ember-cli-string-utils: - specifier: ^1.1.0 - version: 1.1.0 - ember-cli-version-checker: - specifier: ^5.1.2 - version: 5.1.2 - git-repo-info: - specifier: ^2.1.1 - version: 2.1.1 - glob: - specifier: ^9.3.4 - version: 9.3.4 - npm-git-info: - specifier: ^1.0.3 - version: 1.0.3 - semver: - specifier: ^7.3.8 - version: 7.3.8 - silent-error: - specifier: ^1.1.1 - version: 1.1.1 - - packages/request: - dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember/test-waiters': - specifier: ^3.0.2 - version: 3.0.2 - '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^5.3.0 + version: 5.3.0 + debug: + specifier: ^4.3.7 + version: 4.3.7(supports-color@8.1.1) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + tmp: + specifier: ^0.2.3 + version: 0.2.3 devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^7.24.5 + version: 7.26.0 + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + bun-types: + specifier: ^1.1.30 + version: 1.1.34 + ember-cli-test-loader: + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@warp-drive/build-config': injected: true - packages/request-utils: + packages/graph: dependencies: - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.24.1 - version: 7.24.5(@babel/core@7.24.5) '@babel/core': - specifier: ^7.24.4 - version: 7.24.5 - '@babel/plugin-proposal-decorators': - specifier: ^7.24.1 - version: 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-class-properties': - specifier: ^7.24.1 - version: 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-runtime': - specifier: ^7.24.3 - version: 7.24.3(@babel/core@7.24.5) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.24.4 - version: 7.24.5(@babel/core@7.24.5) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.24.4 - version: 7.24.5(@babel/core@7.24.5) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': specifier: ^7.24.1 - version: 7.24.1(@babel/core@7.24.5) - '@babel/runtime': - specifier: ^7.24.4 - version: 7.24.5 - '@embroider/addon-dev': - specifier: ^4.3.1 - version: 4.3.1(rollup@4.17.2) + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.24.5) - '@rollup/plugin-babel': - specifier: ^6.0.4 - version: 6.0.4(@babel/core@7.24.5)(rollup@4.17.2) - '@rollup/plugin-node-resolve': - specifier: ^15.2.3 - version: 15.2.3(rollup@4.17.2) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~5.7.0 - version: 5.7.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(rsvp@4.8.5)(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) pnpm-sync-dependencies-meta-injected: - specifier: 0.0.10 - version: 0.0.10 - rollup: - specifier: ^4.14.3 - version: 4.17.2 - rsvp: - specifier: ^4.8.5 - version: 4.8.5 + specifier: 0.0.14 + version: 0.0.14 typescript: specifier: ^5.4.5 - version: 5.4.5 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) + dependenciesMeta: + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true - packages/serializer: + packages/holodeck: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-test-info: - specifier: ^1.0.0 - version: 1.0.0 + '@hono/node-server': + specifier: ^1.11.1 + version: 1.13.7(hono@4.6.9) + chalk: + specifier: ^5.3.0 + version: 5.3.0 + hono: + specifier: ^4.6.5 + version: 4.6.9 devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) - '@glimmer/component': - specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) - ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/request': + injected: true + '@warp-drive/core-types': injected: true - packages/store: + packages/json-api: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cached-decorator-polyfill: - specifier: ^1.0.1 - version: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + expect-type: + specifier: ^0.20.0 + version: 0.20.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/graph': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - packages/tracking: + packages/legacy-compat: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: - '@babel/cli': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-proposal-class-properties': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': - specifier: ^7.21.0 - version: 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': - specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.21.4 - version: 7.21.4(@babel/core@7.21.4) - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@embroider/addon-dev': - specifier: ^3.0.0 - version: 3.0.0(rollup@3.20.2) - '@rollup/plugin-babel': - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.21.4)(rollup@3.20.2) - '@rollup/plugin-node-resolve': - specifier: ^15.0.1 - version: 15.0.1(rollup@3.20.2) - rollup: - specifier: ^3.20.2 - version: 3.20.2 - tslib: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ^5.0.3 - version: 5.0.3 - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - packages/unpublished-eslint-rules: {} - - packages/unpublished-test-infra: + packages/model: dependencies: - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 - broccoli-merge-trees: - specifier: ^4.2.0 - version: 4.2.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-blueprint-test-helpers: - specifier: ^0.19.2 - version: 0.19.2 - ember-get-config: - specifier: ^2.1.1 - version: 2.1.1 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 - semver: - specifier: ^7.3.8 - version: 7.3.8 - testem: - specifier: ^3.10.1 - version: 3.10.1 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + ember-cli-test-info: + specifier: ^1.0.0 + version: 1.0.0 + inflection: + specifier: ~3.0.0 + version: 3.0.0 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 - '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@glimmer/component': - specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - '@types/semver': - specifier: ^7.3.13 - version: 7.3.13 - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-cli-test-loader: - specifier: ^3.0.0 - version: 3.0.0 - ember-disable-prototype-extensions: - specifier: ^1.1.3 - version: 1.1.3 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - dependenciesMeta: - '@ember-data/private-build-infra': - injected: 'true' - - tests/adapter-encapsulation: - dependencies: - '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/debug': - specifier: workspace:4.12.8 - version: file:packages/debug(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.77.0) + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@ember-data/graph': - specifier: workspace:4.12.8 - version: file:packages/graph(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/json-api': - specifier: workspace:4.12.8 - version: file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../../packages/request - '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^3.0.1 || ^4.0.0 - version: 3.1.1 - '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@3.1.1)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + decorator-transforms: + specifier: ^2.2.2 + version: 2.3.0(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + expect-type: + specifier: ^0.20.0 + version: 0.20.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/debug': - injected: true '@ember-data/graph': injected: true '@ember-data/json-api': injected: true '@ember-data/legacy-compat': injected: true - '@ember-data/model': - injected: true '@ember-data/request': injected: true - '@ember-data/serializer': + '@ember-data/request-utils': injected: true '@ember-data/store': injected: true '@ember-data/tracking': injected: true - '@ember-data/unpublished-test-infra': + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - tests/blueprints: + packages/request: + dependencies: + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.24.5 - '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@packages+store)(@ember/string@4.0.0)(ember-inflector@4.0.2) - '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: link:../../packages/legacy-compat - '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.24.5)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/store@packages+store)(@ember-data/tracking@packages+tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0)) - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@packages+store)(@ember/string@4.0.0)(ember-inflector@4.0.2) - '@ember-data/store': - specifier: workspace:4.12.8 - version: link:../../packages/store - '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) + dependenciesMeta: + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + + packages/request-utils: + dependencies: + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: 3.1.1 + version: 3.1.1(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.24.5) - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-blueprint-test-helpers: - specifier: ^0.19.2 - version: 0.19.2 + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + specifier: 4.0.3 + version: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0) - mocha: - specifier: ^10.2.0 - version: 10.2.0 - silent-error: - specifier: ^1.1.1 - version: 1.1.1 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/adapter': + '@warp-drive/build-config': injected: true - '@ember-data/model': + '@warp-drive/core-types': + injected: true + + packages/rest: + dependencies: + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) + dependenciesMeta: + '@ember-data/request': injected: true - '@ember-data/private-build-infra': + '@ember-data/request-utils': injected: true - '@ember-data/serializer': + '@ember-data/store': injected: true - '@ember-data/unpublished-test-infra': + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - tests/debug-encapsulation: + packages/serializer: + dependencies: + '@ember/edition-utils': + specifier: 1.2.0 + version: 1.2.0 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: + specifier: ^1.0.0 + version: 1.0.0 + ember-cli-string-utils: + specifier: ^1.1.0 + version: 1.1.0 + ember-cli-test-info: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: link:../../packages/legacy-compat + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 - '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + decorator-transforms: + specifier: ^2.2.2 + version: 2.3.0(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/adapter': + '@ember-data/legacy-compat': injected: true '@ember-data/model': injected: true - '@ember-data/serializer': + '@ember-data/request': + injected: true + '@ember-data/request-utils': injected: true '@ember-data/store': injected: true '@ember-data/tracking': injected: true - '@ember-data/unpublished-test-infra': + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - tests/docs: - devDependencies: - qunit: - specifier: ^2.19.4 - version: 2.19.4 - - tests/embroider-basic-compat: + packages/store: dependencies: - '@ember/string': - specifier: ^3.0.1 || ^4.0.0 - version: 3.1.1 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-data: - specifier: workspace:4.12.8 - version: file:packages/-ember-data(@babel/core@7.21.4)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(webpack@5.77.0) - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@embroider/compat': - specifier: ^2.1.1 - version: 2.1.1(@embroider/core@2.1.1) - '@embroider/core': - specifier: ^2.1.1 - version: 2.1.1 - '@embroider/webpack': - specifier: ^2.1.1 - version: 2.1.1(@embroider/core@2.1.1)(webpack@5.77.0) + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - '@types/ember': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember-qunit': - specifier: ^5.0.2 - version: 5.0.2(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@types/ember-testing-helpers': - specifier: ^0.0.4 - version: 0.0.4 - '@types/rsvp': - specifier: ^4.0.4 - version: 4.0.4 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-fastboot: - specifier: ^4.1.0 - version: 4.1.0 - ember-cli-fastboot-testing: - specifier: ^0.6.0 - version: 0.6.0(webpack@5.77.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@3.1.1)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-simple-tree: - specifier: ^0.8.3 - version: 0.8.3(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + decorator-transforms: + specifier: ^2.2.2 + version: 2.3.0(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + expect-type: + specifier: ^0.20.0 + version: 0.20.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ~5.0.3 - version: 5.0.3 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/unpublished-test-infra': + '@ember-data/request': injected: true - ember-data: + '@ember-data/request-utils': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': injected: true - tests/fastboot: + packages/tracking: dependencies: - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra - '@ember/string': - specifier: ^3.0.1 || ^4.0.0 - version: 3.1.1 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-data: - specifier: workspace:4.12.8 - version: file:packages/-ember-data(@babel/core@7.21.4)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(webpack@5.77.0) - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - '@types/ember': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember-qunit': - specifier: ^5.0.2 - version: 5.0.2(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@types/ember-testing-helpers': - specifier: ^0.0.4 - version: 0.0.4 - '@types/rsvp': - specifier: ^4.0.4 - version: 4.0.4 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-fastboot: - specifier: ^4.1.0 - version: 4.1.0 - ember-cli-fastboot-testing: - specifier: ^0.6.0 - version: 0.6.0(webpack@5.77.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-cli-version-checker: - specifier: ^5.1.2 - version: 5.1.2 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@3.1.1)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-simple-tree: - specifier: ^0.8.3 - version: 0.8.3(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/validator': + specifier: ^0.92.3 + version: 0.92.3 + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 typescript: - specifier: ~5.0.3 - version: 5.0.3 - webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/unpublished-test-infra': + '@warp-drive/build-config': injected: true - ember-data: + '@warp-drive/core-types': injected: true - tests/full-data-asset-size-app: + packages/unpublished-eslint-rules: {} + + packages/unpublished-test-infra: + dependencies: + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + chalk: + specifier: ^4.1.2 + version: 4.1.2 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + semver: + specifier: ^7.6.3 + version: 7.6.3 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.24.5 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.24.1 + version: 7.26.0(@babel/core@7.26.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/tracking': - specifier: ^1.1.2 - version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-terser: - specifier: ^4.0.2 - version: 4.0.2 - ember-data: - specifier: workspace:4.12.8 - version: link:../../packages/-ember-data - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + version: 1.1.2(@babel/core@7.26.0) + '@types/qunit': + specifier: 2.19.10 + version: 2.19.10 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - webpack: - specifier: ^5.77.0 - version: 5.77.0 - zlib: - specifier: 1.0.5 - version: 1.0.5 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + vite: + specifier: ^5.2.11 + version: 5.4.11(@types/node@20.17.6) dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/build-config': injected: true - ember-data: + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': injected: true - tests/graph: + tests/blueprints: devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/adapter': + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/graph': - specifier: workspace:4.12.8 - version: file:packages/graph(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/json-api': - specifier: workspace:4.12.8 - version: file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../../packages/request + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 - '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ~5.12.0 + version: 5.12.0 ember-cli-blueprint-test-helpers: specifier: ^0.19.2 - version: 0.19.2 - ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) - ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-cli-inject-live-reload: - specifier: ^2.1.0 - version: 2.1.0 - ember-cli-sri: - specifier: ^2.1.1 - version: 2.1.1 - ember-cli-terser: - specifier: ~4.0.2 - version: 4.0.2 - ember-cli-test-loader: - specifier: ^3.0.0 - version: 3.0.0 - ember-disable-prototype-extensions: - specifier: ^1.1.3 - version: 1.1.3 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 - ember-load-initializers: - specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) - ember-maybe-import-regenerator: - specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) - ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + version: 0.19.2(ember-cli@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - ember-source-channel-url: - specifier: ^3.0.0 - version: 3.0.0 - ember-try: - specifier: ^2.0.0 - version: 2.0.0 - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-console-grouper: - specifier: ^0.3.0 - version: 0.3.0 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + mocha: + specifier: ^10.7.3 + version: 10.8.2 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 silent-error: specifier: ^1.1.1 version: 1.1.1 - webpack: - specifier: ^5.77.0 - version: 5.77.0 dependenciesMeta: + '@ember-data/adapter': + injected: true + '@ember-data/debug': + injected: true '@ember-data/graph': injected: true '@ember-data/json-api': @@ -2100,148 +1686,180 @@ importers: injected: true '@ember-data/model': injected: true - '@ember-data/private-build-infra': - injected: true '@ember-data/request': injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/serializer': + injected: true '@ember-data/store': injected: true '@ember-data/tracking': injected: true '@ember-data/unpublished-test-infra': injected: true + '@warp-drive/core-types': + injected: true + ember-cli: + injected: true + ember-cli-blueprint-test-helpers: + injected: true - tests/json-api: + tests/builders: devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/active-record': + specifier: workspace:* + version: file:packages/active-record(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/graph': - specifier: workspace:4.12.8 - version: file:packages/graph(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/json-api': - specifier: workspace:4.12.8 - version: file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/rest': + specifier: workspace:* + version: file:packages/rest(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^3.1.1 + version: 3.1.1(@babel/core@7.26.0) '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-blueprint-test-helpers: - specifier: ^0.19.2 - version: 0.19.2 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 - ember-cli-sri: - specifier: ^2.1.1 - version: 2.1.1 - ember-cli-terser: - specifier: ~4.0.2 - version: 4.0.2 ember-cli-test-loader: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) ember-disable-prototype-extensions: specifier: ^1.1.3 version: 1.1.3 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^4.0.3 + version: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) ember-source-channel-url: specifier: ^3.0.0 version: 3.0.0 - ember-try: - specifier: ^2.0.0 - version: 2.0.0 loader.js: specifier: ^4.7.0 version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-console-grouper: - specifier: ^0.3.0 - version: 0.3.0 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 silent-error: specifier: ^1.1.1 version: 1.1.1 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: + '@ember-data/active-record': + injected: true + '@ember-data/debug': + injected: true '@ember-data/graph': injected: true '@ember-data/json-api': injected: true - '@ember-data/private-build-infra': + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/rest': injected: true '@ember-data/store': injected: true @@ -2249,125 +1867,156 @@ importers: injected: true '@ember-data/unpublished-test-infra': injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': + injected: true - tests/json-api-encapsulation: + tests/docs: devDependencies: + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + + tests/ember-data__adapter: + dependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: ^7.24.5 + version: 7.26.0 '@ember-data/debug': - specifier: workspace:4.12.8 - version: file:packages/debug(@ember-data/store@4.12.8)(@ember/string@4.0.0)(webpack@5.77.0) + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: link:../../packages/legacy-compat + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../../packages/request + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + ember-cli-test-loader: + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) loader.js: specifier: ^4.7.0 version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: - '@ember-data/adapter': - injected: true '@ember-data/debug': injected: true + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true '@ember-data/legacy-compat': injected: true '@ember-data/model': injected: true '@ember-data/request': injected: true + '@ember-data/request-utils': + injected: true '@ember-data/serializer': injected: true '@ember-data/store': @@ -2376,177 +2025,148 @@ importers: injected: true '@ember-data/unpublished-test-infra': injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': + injected: true + '@warp-rive/build-config': + injected: true - tests/main: + tests/ember-data__graph: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 - '@babel/plugin-transform-typescript': - specifier: ^7.21.3 - version: 7.21.3(@babel/core@7.21.4) + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 '@embroider/macros': - specifier: 1.10.0 - version: 1.10.0 + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) - '@glimmer/env': - specifier: ^0.1.7 - version: 0.1.7 + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - '@types/ember': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember-qunit': - specifier: ^5.0.2 - version: 5.0.2(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@types/ember-testing-helpers': - specifier: ^0.0.4 - version: 0.0.4 - '@types/ember__debug': - specifier: ^4.0.3 - version: 4.0.3(@babel/core@7.21.4) - '@types/ember__object': - specifier: ^4.0.5 - version: 4.0.5(@babel/core@7.21.4) - '@types/ember__utils': - specifier: ^4.0.2 - version: 4.0.2(@babel/core@7.21.4) - '@types/qunit': - specifier: ^2.19.4 - version: 2.19.4 - '@types/rsvp': - specifier: ^4.0.4 - version: 4.0.4 - broccoli-concat: - specifier: ^4.2.5 - version: 4.2.5 - broccoli-merge-trees: - specifier: ^4.2.0 - version: 4.2.0 - broccoli-stew: - specifier: ^3.0.0 - version: 3.0.0 - broccoli-string-replace: - specifier: ^0.1.2 - version: 0.1.2 - broccoli-test-helper: - specifier: ^2.0.0 - version: 2.0.0 - broccoli-uglify-sourcemap: - specifier: ^4.0.0 - version: 4.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-cached-decorator-polyfill: - specifier: ^1.0.1 - version: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 - ember-cli-terser: - specifier: ~4.0.2 - version: 4.0.2 ember-cli-test-loader: - specifier: ^3.0.0 - version: 3.0.0 - ember-data: - specifier: workspace:4.12.8 - version: link:../../packages/-ember-data - ember-decorators-polyfill: - specifier: ^1.1.5 - version: 1.1.5(@babel/core@7.21.4) + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) ember-disable-prototype-extensions: specifier: ^1.1.3 version: 1.1.3 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) ember-source-channel-url: specifier: ^3.0.0 version: 3.0.0 - ember-strict-resolver: - specifier: ^1.3.0 - version: 1.3.0 ember-try: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^3.0.0 + version: 3.0.0 loader.js: specifier: ^4.7.0 version: 4.7.0 - pretender: - specifier: ^3.4.7 - version: 3.4.7 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + silent-error: + specifier: ^1.1.1 + version: 1.1.1 typescript: - specifier: ~5.0.3 - version: 5.0.3 + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: - '@ember-data/adapter': - injected: true '@ember-data/debug': injected: true '@ember-data/graph': @@ -2557,9 +2177,9 @@ importers: injected: true '@ember-data/model': injected: true - '@ember-data/private-build-infra': + '@ember-data/request': injected: true - '@ember-data/serializer': + '@ember-data/request-utils': injected: true '@ember-data/store': injected: true @@ -2567,113 +2187,162 @@ importers: injected: true '@ember-data/unpublished-test-infra': injected: true - ember-data: + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': injected: true - tests/model-encapsulation: + tests/ember-data__json-api: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra - '@ember-data/serializer': - specifier: workspace:4.12.8 - version: file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': + specifier: ^1.2.0 + version: 1.2.0 '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/holodeck': + specifier: workspace:* + version: file:packages/holodeck(@ember-data/request@4.12.8)(@warp-drive/core-types@4.12.8) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + ember-cli-test-loader: + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + ember-disable-prototype-extensions: + specifier: ^1.1.3 + version: 1.1.3 ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + ember-source-channel-url: + specifier: ^3.0.0 + version: 3.0.0 + ember-try: + specifier: ^3.0.0 + version: 3.0.0 loader.js: specifier: ^4.7.0 version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + silent-error: + specifier: ^1.1.1 + version: 1.1.1 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: - '@ember-data/adapter': + '@ember-data/debug': + injected: true + '@ember-data/graph': injected: true - '@ember-data/private-build-infra': + '@ember-data/json-api': injected: true - '@ember-data/serializer': + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': injected: true '@ember-data/store': injected: true @@ -2681,139 +2350,227 @@ importers: injected: true '@ember-data/unpublished-test-infra': injected: true - - tests/performance: - dependencies: - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 - ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) - ember-data: - specifier: workspace:4.12.8 - version: link:../../packages/-ember-data + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': + injected: true + '@warp-drive/holodeck': + injected: true + + tests/ember-data__model: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/adapter': + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 + specifier: ^6.3.0 + version: 6.3.0 + ember-cli-inject-live-reload: + specifier: ^2.1.0 + version: 2.1.0 + ember-cli-test-loader: + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) loader.js: specifier: ^4.7.0 version: 4.7.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 - zlib: - specifier: 1.0.5 - version: 1.0.5 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: - ember-data: + '@ember-data/adapter': + injected: true + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/legacy-compat': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/serializer': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': injected: true - tests/request: + tests/ember-data__request: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 - '@ember-data/private-build-infra': - specifier: workspace:4.12.8 - version: file:packages/private-build-infra + specifier: ^7.24.5 + version: 7.26.0 '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../../packages/request - '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) '@ember/edition-utils': specifier: ^1.2.0 version: 1.2.0 '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 - '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': + specifier: workspace:* + version: file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + '@warp-drive/holodeck': + specifier: workspace:* + version: file:packages/holodeck(@ember-data/request@4.12.8)(@warp-drive/core-types@4.12.8) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + bun-types: + specifier: ^1.1.30 + version: 1.1.34 ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 - ember-cli-blueprint-test-helpers: - specifier: ^0.19.2 - version: 0.19.2 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 @@ -2824,174 +2581,351 @@ importers: specifier: ~4.0.2 version: 4.0.2 ember-cli-test-loader: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) ember-disable-prototype-extensions: specifier: ^1.1.3 version: 1.1.3 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 - ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 - ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + version: 1.0.0(@babel/core@7.26.0) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) ember-source-channel-url: specifier: ^3.0.0 version: 3.0.0 ember-try: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^3.0.0 + version: 3.0.0 loader.js: specifier: ^4.7.0 version: 4.7.0 - qunit: - specifier: ^2.19.4 - version: 2.19.4 - qunit-console-grouper: - specifier: ^0.3.0 - version: 0.3.0 - qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 silent-error: specifier: ^1.1.1 version: 1.1.1 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + webpack: + specifier: 5.94.0 + version: 5.94.0 + dependenciesMeta: + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/diagnostic': + injected: true + '@warp-drive/holodeck': + injected: true + + tests/ember-data__serializer: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/runtime': + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/adapter': + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/unpublished-test-infra': + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-dependency-checker: + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-cli-inject-live-reload: + specifier: ^2.1.0 + version: 2.1.0 + ember-load-initializers: + specifier: ^2.1.2 + version: 2.1.2(@babel/core@7.26.0) + ember-maybe-import-regenerator: + specifier: ^1.0.0 + version: 1.0.0(@babel/core@7.26.0) + ember-qunit: + specifier: 8.0.2 + version: 8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4) + ember-resolver: + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + qunit-dom: + specifier: ^3.1.1 + version: 3.3.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: - '@ember-data/private-build-infra': + '@ember-data/adapter': + injected: true + '@ember-data/debug': + injected: true + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': injected: true '@ember-data/request': injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true '@ember-data/unpublished-test-infra': injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true - tests/serializer-encapsulation: + tests/embroider-basic-compat: devDependencies: '@babel/core': - specifier: ^7.21.4 - version: 7.21.4 + specifier: ^7.24.5 + version: 7.26.0 '@babel/runtime': - specifier: ^7.21.0 - version: 7.21.0 + specifier: ^7.24.5 + version: 7.26.0 '@ember-data/adapter': - specifier: workspace:4.12.8 - version: file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2) + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/graph': - specifier: workspace:4.12.8 - version: file:packages/graph(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/json-api': - specifier: workspace:4.12.8 - version: file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) '@ember-data/legacy-compat': - specifier: workspace:4.12.8 - version: file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/model': - specifier: workspace:4.12.8 - version: file:packages/model(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/request': - specifier: workspace:4.12.8 - version: link:../../packages/request + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/store': - specifier: workspace:4.12.8 - version: file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/tracking': - specifier: workspace:4.12.8 - version: link:../../packages/tracking + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) '@ember-data/unpublished-test-infra': - specifier: workspace:4.12.8 - version: file:packages/unpublished-test-infra + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) '@ember/optional-features': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.0 + version: 2.2.0 '@ember/string': - specifier: ^4.0.0 - version: 4.0.0 + specifier: ^3.1.1 + version: 3.1.1(@babel/core@7.26.0) '@ember/test-helpers': - specifier: ^2.9.3 - version: 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/compat': + specifier: ^3.6.5 + version: 3.7.0(@embroider/core@3.4.19) + '@embroider/core': + specifier: ^3.4.19 + version: 3.4.19 + '@embroider/webpack': + specifier: ^4.0.8 + version: 4.0.8(@embroider/core@3.4.19)(webpack@5.94.0) '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.21.4) + version: 1.1.2(@babel/core@7.26.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 - broccoli-asset-rev: - specifier: ^3.0.0 - version: 3.0.0 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config ember-auto-import: - specifier: ^2.6.1 - version: 2.6.1(webpack@5.77.0) + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) ember-cli: - specifier: ~4.11.0 - version: 4.11.0 - ember-cli-app-version: - specifier: ^6.0.0 - version: 6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ~5.12.0 + version: 5.12.0 ember-cli-babel: - specifier: ^7.26.11 - version: 7.26.11 + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) ember-cli-dependency-checker: - specifier: ^3.3.1 - version: 3.3.1(ember-cli@4.11.0) + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-fastboot: + specifier: ^4.1.5 + version: 4.1.5(@babel/core@7.26.0)(ember-cli@5.12.0)(ember-source@5.12.0) + ember-cli-fastboot-testing: + specifier: ^0.6.2 + version: 0.6.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-fastboot@4.1.5)(ember-cli@5.12.0)(ember-source@5.12.0) ember-cli-htmlbars: - specifier: ^6.2.0 - version: 6.2.0 + specifier: ^6.3.0 + version: 6.3.0 ember-cli-inject-live-reload: specifier: ^2.1.0 version: 2.1.0 - ember-export-application-global: - specifier: ^2.0.1 - version: 2.0.1 + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@3.1.1)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-inflector@4.0.3)(ember-source@5.12.0)(qunit@2.19.4) ember-inflector: - specifier: ^4.0.2 - version: 4.0.2 + specifier: 4.0.3 + version: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) ember-load-initializers: specifier: ^2.1.2 - version: 2.1.2(@babel/core@7.21.4) + version: 2.1.2(@babel/core@7.26.0) ember-maybe-import-regenerator: specifier: ^1.0.0 - version: 1.0.0 + version: 1.0.0(@babel/core@7.26.0) ember-qunit: - specifier: ^6.2.0 - version: 6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0) + specifier: 8.0.2 + version: 8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4) ember-resolver: - specifier: ^10.0.0 - version: 10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-simple-tree: + specifier: ^0.8.4 + version: 0.8.4(@babel/core@7.26.0) ember-source: - specifier: ~4.12.0 - version: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) loader.js: specifier: ^4.7.0 version: 4.7.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 qunit: - specifier: ^2.19.4 - version: 2.19.4 + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) qunit-dom: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^3.1.1 + version: 3.3.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 webpack: - specifier: ^5.77.0 - version: 5.77.0 + specifier: 5.94.0 + version: 5.94.0 dependenciesMeta: '@ember-data/adapter': injected: true + '@ember-data/debug': + injected: true '@ember-data/graph': injected: true '@ember-data/json-api': @@ -3002,19568 +2936,17094 @@ importers: injected: true '@ember-data/request': injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/serializer': + injected: true '@ember-data/store': injected: true '@ember-data/tracking': injected: true '@ember-data/unpublished-test-infra': injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + ember-data: + injected: true -packages: - - '@ampproject/remapping@2.2.0': - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} - - '@babel/cli@7.21.0': - resolution: {integrity: sha512-xi7CxyS8XjSyiwUGCfwf+brtJxjW1/ZTcBUkP10xawIEXLX5HzLn+3aXkgxozcP2UhRhtKTmQurw9Uaes7jZrA==} - engines: {node: '>=6.9.0'} - hasBin: true - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/cli@7.24.5': - resolution: {integrity: sha512-2qg1mYtJRsOOWF6IUwLP5jI42P8Cc0hQ5TmnjLrik/4DKouO8dFJN80HEz81VmVeUs97yuuf3vQ/9j7Elrcjlg==} - engines: {node: '>=6.9.0'} - hasBin: true - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/code-frame@7.18.6': - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.21.4': - resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.24.2': - resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.21.4': - resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.24.4': - resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.21.4': - resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.24.5': - resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} - engines: {node: '>=6.9.0'} - - '@babel/eslint-parser@7.21.3': - resolution: {integrity: sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} - peerDependencies: - '@babel/core': '>=7.11.0' - eslint: ^7.5.0 || ^8.0.0 - - '@babel/generator@7.21.4': - resolution: {integrity: sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.5': - resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.18.6': - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.22.5': - resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15': - resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.21.4': - resolution: {integrity: sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-compilation-targets@7.23.6': - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-create-class-features-plugin@7.21.0': - resolution: {integrity: sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-class-features-plugin@7.24.5': - resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-regexp-features-plugin@7.21.0': - resolution: {integrity: sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-regexp-features-plugin@7.22.15': - resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.3.3': - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} - peerDependencies: - '@babel/core': ^7.4.0-0 - - '@babel/helper-define-polyfill-provider@0.6.2': - resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - '@babel/helper-environment-visitor@7.18.9': - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-environment-visitor@7.22.20': - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.21.0': - resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.23.0': - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.18.6': - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.22.5': - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.21.0': - resolution: {integrity: sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.24.5': - resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.18.6': - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.21.4': - resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.24.3': - resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.21.2': - resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.24.5': - resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-optimise-call-expression@7.18.6': - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-optimise-call-expression@7.22.5': - resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.20.2': - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.24.5': - resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-remap-async-to-generator@7.22.20': - resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-replace-supers@7.20.7': - resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} - engines: {node: '>=6.9.0'} - - '@babel/helper-replace-supers@7.24.1': - resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-simple-access@7.20.2': - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.24.5': - resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-skip-transparent-expression-wrappers@7.20.0': - resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-skip-transparent-expression-wrappers@7.22.5': - resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.18.6': - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.24.5': - resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.19.4': - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.24.1': - resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.19.1': - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.24.5': - resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.21.0': - resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.23.5': - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-wrap-function@7.24.5': - resolution: {integrity: sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.21.0': - resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.24.5': - resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.18.6': - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.24.5': - resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.21.3': - resolution: {integrity: sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.21.4': - resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.24.5': - resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5': - resolution: {integrity: sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6': - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1': - resolution: {integrity: sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7': - resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1': - resolution: {integrity: sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1': - resolution: {integrity: sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-proposal-async-generator-functions@7.20.7': - resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-static-block@7.21.0': - resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-proposal-decorators@7.21.0': - resolution: {integrity: sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-decorators@7.24.1': - resolution: {integrity: sha512-zPEvzFijn+hRvJuX2Vu3KbEBN39LN3f7tW3MQO2LsIs57B26KU+kUc82BdAktS1VCM6libzh45eKGI65lg0cpA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-dynamic-import@7.18.6': - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-export-namespace-from@7.18.9': - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-json-strings@7.18.6': - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-logical-assignment-operators@7.20.7': - resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-numeric-separator@7.18.6': - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.20.7': - resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-catch-binding@7.18.6': - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.21.0': - resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-methods@7.18.6': - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-property-in-object@7.21.0': - resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-unicode-property-regex@7.18.6': - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-decorators@7.21.0': - resolution: {integrity: sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-decorators@7.24.1': - resolution: {integrity: sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.20.0': - resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.24.1': - resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.24.1': - resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.21.4': - resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.24.1': - resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.20.0': - resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.24.1': - resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-arrow-functions@7.20.7': - resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-arrow-functions@7.24.1': - resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-generator-functions@7.24.3': - resolution: {integrity: sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.20.7': - resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.24.1': - resolution: {integrity: sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.18.6': - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.24.1': - resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.21.0': - resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.24.5': - resolution: {integrity: sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-properties@7.24.1': - resolution: {integrity: sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-static-block@7.24.4': - resolution: {integrity: sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-transform-classes@7.21.0': - resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-classes@7.24.5': - resolution: {integrity: sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.20.7': - resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.24.1': - resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.21.3': - resolution: {integrity: sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.24.5': - resolution: {integrity: sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.18.6': - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.24.1': - resolution: {integrity: sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.18.9': - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.24.1': - resolution: {integrity: sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dynamic-import@7.24.1': - resolution: {integrity: sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.18.6': - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.24.1': - resolution: {integrity: sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-export-namespace-from@7.24.1': - resolution: {integrity: sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.21.0': - resolution: {integrity: sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.24.1': - resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-function-name@7.18.9': - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-function-name@7.24.1': - resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-json-strings@7.24.1': - resolution: {integrity: sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.18.9': - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.24.1': - resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-logical-assignment-operators@7.24.1': - resolution: {integrity: sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.18.6': - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.24.1': - resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.20.11': - resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.24.1': - resolution: {integrity: sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.21.2': - resolution: {integrity: sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.24.1': - resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.20.11': - resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.24.1': - resolution: {integrity: sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.18.6': - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.24.1': - resolution: {integrity: sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.20.5': - resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.22.5': - resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-new-target@7.18.6': - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-new-target@7.24.1': - resolution: {integrity: sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-nullish-coalescing-operator@7.24.1': - resolution: {integrity: sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-numeric-separator@7.24.1': - resolution: {integrity: sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-rest-spread@7.24.5': - resolution: {integrity: sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.18.6': - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.24.1': - resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-catch-binding@7.24.1': - resolution: {integrity: sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-chaining@7.24.5': - resolution: {integrity: sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.21.3': - resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.24.5': - resolution: {integrity: sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-methods@7.24.1': - resolution: {integrity: sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-property-in-object@7.24.5': - resolution: {integrity: sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.18.6': - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.24.1': - resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.20.5': - resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.24.1': - resolution: {integrity: sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.18.6': - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.24.1': - resolution: {integrity: sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-runtime@7.21.4': - resolution: {integrity: sha512-1J4dhrw1h1PqnNNpzwxQ2UBymJUF8KuPjAAnlLwZcGhHAIqUigFW7cdK6GHoB64ubY4qXQNYknoUeks4Wz7CUA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-runtime@7.24.3': - resolution: {integrity: sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.18.6': - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.24.1': - resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.20.7': - resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.24.1': - resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.18.6': - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.24.1': - resolution: {integrity: sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.18.9': - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.24.1': - resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.18.9': - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.24.5': - resolution: {integrity: sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.21.3': - resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.24.5': - resolution: {integrity: sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.4.5': - resolution: {integrity: sha512-RPB/YeGr4ZrFKNwfuQRlMf2lxoCUaU01MTw39/OFE/RiL8HDjtn68BwEPft1P7JN4akyEmjGWAMNldOV7o9V2g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.5.5': - resolution: {integrity: sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.18.10': - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.24.1': - resolution: {integrity: sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-property-regex@7.24.1': - resolution: {integrity: sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.18.6': - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.24.1': - resolution: {integrity: sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-sets-regex@7.24.1': - resolution: {integrity: sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/polyfill@7.12.1': - resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} - deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. - - '@babel/preset-env@7.21.4': - resolution: {integrity: sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-env@7.24.5': - resolution: {integrity: sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.5': - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - - '@babel/preset-typescript@7.21.4': - resolution: {integrity: sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.24.1': - resolution: {integrity: sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/regjsgen@0.8.0': - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - - '@babel/runtime@7.12.18': - resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==} - - '@babel/runtime@7.21.0': - resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.24.5': - resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.20.7': - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.24.0': - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.21.3': - resolution: {integrity: sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.21.4': - resolution: {integrity: sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.24.5': - resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.21.3': - resolution: {integrity: sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.21.4': - resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.24.5': - resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} - engines: {node: '>=6.9.0'} - - '@cnakazawa/watch@1.0.4': - resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} - engines: {node: '>=0.1.95'} - hasBin: true - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@ember-data/adapter@file:packages/adapter': - resolution: {directory: packages/adapter, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - ember-inflector: ^4.0.2 - - '@ember-data/debug@file:packages/debug': - resolution: {directory: packages/debug, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - - '@ember-data/graph@file:packages/graph': - resolution: {directory: packages/graph, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': workspace:4.12.8 - - '@ember-data/json-api@file:packages/json-api': - resolution: {directory: packages/json-api, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/graph': workspace:4.12.8 - '@ember-data/store': workspace:4.12.8 - - '@ember-data/legacy-compat@file:packages/legacy-compat': - resolution: {directory: packages/legacy-compat, type: directory} - engines: {node: 16.* || >= 18} - peerDependencies: - '@ember-data/graph': workspace:4.12.8 - '@ember-data/json-api': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - peerDependenciesMeta: - '@ember-data/graph': - optional: true - '@ember-data/json-api': - optional: true - - '@ember-data/model@file:packages/model': - resolution: {directory: packages/model, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/debug': workspace:4.12.8 - '@ember-data/graph': workspace:4.12.8 - '@ember-data/json-api': workspace:4.12.8 - '@ember-data/legacy-compat': workspace:4.12.8 - '@ember-data/store': workspace:4.12.8 - '@ember-data/tracking': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - ember-inflector: ^4.0.2 - peerDependenciesMeta: - '@ember-data/debug': - optional: true - '@ember-data/graph': - optional: true - '@ember-data/json-api': - optional: true - - '@ember-data/private-build-infra@file:packages/private-build-infra': - resolution: {directory: packages/private-build-infra, type: directory} - engines: {node: 16.* || >= 18.*} - - '@ember-data/request@file:packages/request': - resolution: {directory: packages/request, type: directory} - engines: {node: 16.* || >= 18} - - '@ember-data/rfc395-data@0.0.4': - resolution: {integrity: sha512-tGRdvgC9/QMQSuSuJV45xoyhI0Pzjm7A9o/MVVA3HakXIImJbbzx/k/6dO9CUEQXIyS2y0fW6C1XaYOG7rY0FQ==} - - '@ember-data/serializer@file:packages/serializer': - resolution: {directory: packages/serializer, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - ember-inflector: ^4.0.2 - - '@ember-data/store@file:packages/store': - resolution: {directory: packages/store, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/graph': workspace:4.12.8 - '@ember-data/json-api': workspace:4.12.8 - '@ember-data/legacy-compat': workspace:4.12.8 - '@ember-data/model': workspace:4.12.8 - '@ember-data/tracking': workspace:4.12.8 - '@ember/string': ^3.0.1 || ^4.0.0 - '@glimmer/tracking': ^1.1.2 - peerDependenciesMeta: - '@ember-data/graph': - optional: true - '@ember-data/json-api': - optional: true - '@ember-data/legacy-compat': - optional: true - '@ember-data/model': - optional: true - - '@ember-data/tracking@file:packages/tracking': - resolution: {directory: packages/tracking, type: directory} - engines: {node: 16.* || >= 18} - - '@ember-data/unpublished-test-infra@file:packages/unpublished-test-infra': - resolution: {directory: packages/unpublished-test-infra, type: directory} - engines: {node: 16.* || >= 18.*} - - '@ember/edition-utils@1.2.0': - resolution: {integrity: sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==} - - '@ember/optional-features@2.0.0': - resolution: {integrity: sha512-4gkvuGRYfpAh1nwAz306cmMeC1mG7wxZnbsBZ09mMaMX/W7IyKOKc/38JwrDPUFUalmNEM7q7JEPcmew2M3Dog==} - engines: {node: 10.* || 12.* || >= 14} - - '@ember/string@3.1.1': - resolution: {integrity: sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg==} - engines: {node: 12.* || 14.* || >= 16} - - '@ember/string@4.0.0': - resolution: {integrity: sha512-IMVyVE72twuAMSYcHzWSgtgYTtzlHlKSGW8vEbztnnmkU6uo7kVHmiqSN9R4RkBhzvh0VD4G76Eph+55t3iNIA==} - - '@ember/test-helpers@2.9.3': - resolution: {integrity: sha512-ejVg4Dj+G/6zyLvQsYOvmGiOLU6AS94tY4ClaO1E2oVvjjtVJIRmVLFN61I+DuyBg9hS3cFoPjQRTZB9MRIbxQ==} - engines: {node: 10.* || 12.* || 14.* || 15.* || >= 16.*} - peerDependencies: - ember-source: '>=3.8.0' - - '@ember/test-waiters@3.0.2': - resolution: {integrity: sha512-H8Q3Xy9rlqhDKnQpwt2pzAYDouww4TZIGSI1pZJhM7mQIGufQKuB0ijzn/yugA6Z+bNdjYp1HioP8Y4hn2zazQ==} - engines: {node: 10.* || 12.* || >= 14.*} - - '@embroider/addon-dev@3.0.0': - resolution: {integrity: sha512-h3ISDdp8LASA6583WC3IU3ECZ5fHlW3V3EkgpEeeH7KhxTerHjDjNf+S6+ZvPH+ZHi3WOCYPvUA5OfNICyMbtA==} - engines: {node: 12.* || 14.* || >= 16} - hasBin: true - - '@embroider/addon-dev@4.3.1': - resolution: {integrity: sha512-CNZ4Y69PPIZAAGGoERjvDcrwOwWTuUmnRYu+XnmqKk0opdlu/PTssO9YWyxp8AnvGd2l7iLCjEn5mpLFvifstA==} - engines: {node: 12.* || 14.* || >= 16} - hasBin: true - - '@embroider/babel-loader-8@2.0.0': - resolution: {integrity: sha512-a1bLodfox8JEgNHuhiIBIcXJ4b8NNnKWYkMIpJx216pn80Jf1jcFosQpxnqC8hYHrnG0XRKzQ9zJYgJXoa1wfg==} - engines: {node: 12.* || 14.* || >= 16} - peerDependencies: - '@embroider/core': ^2.0.0 - - '@embroider/compat@2.1.1': - resolution: {integrity: sha512-HNq5vv7NpQ1Jr+4slzmLBqsy5NDsIHilYeQiWboMrPAyHr5NHlKYWciIcmxdgPgz2kf/8D5nDiANgJznZedlyw==} - engines: {node: 12.* || 14.* || >= 16} - hasBin: true - peerDependencies: - '@embroider/core': ^2.0.0 - - '@embroider/core@2.1.1': - resolution: {integrity: sha512-N4rz+r8WjHYmwprvBYC0iUT4EWNpdDjF7JLl8PEYlWbhXDEJL+Ma/aP78S7spMhIpJX9SHK7nbgNxmZAqAe34A==} - engines: {node: 12.* || 14.* || >= 16} - - '@embroider/core@3.4.9': - resolution: {integrity: sha512-+Q1ekptUgUAGYZoDHJ6Ts+KNPXeLbEpQziCutj3NxqT94SuBiL5h6KWDWj86KmrL0gJ4NnRfNrAZt5iV2p1i5A==} - engines: {node: 12.* || 14.* || >= 16} - - '@embroider/hbs-loader@2.0.0': - resolution: {integrity: sha512-rWcZyZ3n35LwlPTS6/fYsdHqPWUh4QO/cVTIJOSeLqJCATNTho7tjBXS6pBvV9cZgvqP/Xph/08xjdUyOWUOxQ==} - engines: {node: 12.* || 14.* || >= 16} - peerDependencies: - '@embroider/core': ^2.0.0 - webpack: ^5 - - '@embroider/macros@1.10.0': - resolution: {integrity: sha512-LMbfQGk/a+f6xtvAv5fq/wf2LRxETnbgSCLUf/z6ebzmuskOUxrke+uP55chF/loWrARi9g6erFQ7RDOUoBMSg==} - engines: {node: 12.* || 14.* || >= 16} - - '@embroider/shared-internals@2.0.0': - resolution: {integrity: sha512-qZ2/xky9mWm5YC6noOa6AiAwgISEQ78YTZNv4SNu2PFgEK/H+Ha/3ddngzGSsnXkVnIHZyxIBzhxETonQYHY9g==} - engines: {node: 12.* || 14.* || >= 16} - - '@embroider/shared-internals@2.6.0': - resolution: {integrity: sha512-A2BYQkhotdKOXuTaxvo9dqOIMbk+2LqFyqvfaaePkZcFJvtCkvTaD31/sSzqvRF6rdeBHjdMwU9Z2baPZ55fEQ==} - engines: {node: 12.* || 14.* || >= 16} - - '@embroider/util@1.10.0': - resolution: {integrity: sha512-utAFKoq6ajI27jyqjvX3PiGL4m+ZyGVlVNbSbE/nOqi2llRyAkh5ltH1WkIK7jhdwQFJouo1NpOSj9J3/HDa3A==} - engines: {node: 14.* || >= 16} - peerDependencies: - '@glint/template': ^1.0.0-beta.1 - ember-source: '*' - peerDependenciesMeta: - '@glint/template': - optional: true - - '@embroider/webpack@2.1.1': - resolution: {integrity: sha512-1IzXXexv/QxDyk4N6kamtiTk92HszlaQZXGB+xhnRCMY4F7Hgxad4gSPvnSy/oSkbHTMWSGjCTS5e4tQcUC8Cg==} - engines: {node: 12.* || 14.* || >= 16} - peerDependencies: - '@embroider/core': ^2.0.0 - webpack: ^5.0.0 - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.4.1': - resolution: {integrity: sha512-BISJ6ZE4xQsuL/FmsyRaiffpq977bMlsKfGHTQrOGFErfByxIe6iZTxPf/00Zon9b9a7iUykfQwejN3s2ZW/Bw==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.0.2': - resolution: {integrity: sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.37.0': - resolution: {integrity: sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@gar/promisify@1.1.3': - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - - '@glimmer/compiler@0.87.1': - resolution: {integrity: sha512-7qXrOv55cH/YW+Vs4dFkNJsNXAW/jP+7kZLhKcH8wCduPfBCQxb9HNh1lBESuFej2rCks6h9I1qXeZHkc/oWxQ==} - engines: {node: '>= 16.0.0'} - - '@glimmer/component@1.1.2': - resolution: {integrity: sha512-XyAsEEa4kWOPy+gIdMjJ8XlzA3qrGH55ZDv6nA16ibalCR17k74BI0CztxuRds+Rm6CtbUVgheCVlcCULuqD7A==} - engines: {node: 6.* || 8.* || >= 10.*} - - '@glimmer/debug@0.87.1': - resolution: {integrity: sha512-rja9/Hofv1NEjIqp8P2eQuHY3+orlS3BL4fbFyvrE+Pw4lRwQPLm6UdgCMHZGGe9yweZAGvNVH6CimDBq7biwA==} - - '@glimmer/destroyable@0.87.1': - resolution: {integrity: sha512-v9kdMq/FCSMcXK4gIKxPCSEcYXjDAnapKVY2o9fCgqky+mbpd0XuGoxaXa35nFwDk69L/9/8B3vXQOpa6ThikA==} - - '@glimmer/di@0.1.11': - resolution: {integrity: sha512-moRwafNDwHTnTHzyyZC9D+mUSvYrs1Ak0tRPjjmCghdoHHIvMshVbEnwKb/1WmW5CUlKc2eL9rlAV32n3GiItg==} - - '@glimmer/encoder@0.87.1': - resolution: {integrity: sha512-5oZEkdtYcAbkiWuXFQ8ofSEGH5uzqi86WK9/IXb7Qn4t6o7ixadWk8nhtORRpVS1u4FpAjhsAysnzRFoNqJwbQ==} - - '@glimmer/env@0.1.7': - resolution: {integrity: sha512-JKF/a9I9jw6fGoz8kA7LEQslrwJ5jms5CXhu/aqkBWk+PmZ6pTl8mlb/eJ/5ujBGTiQzBhy5AIWF712iA+4/mw==} - - '@glimmer/global-context@0.87.1': - resolution: {integrity: sha512-Mitr7pBeVDTplFWlohyzxWLpFll7ffMZN+fnkBmUj8HiDLbD790Lb8lR9B2nL3t4RGnh6W9kDkCnZB+hvDm/eQ==} - - '@glimmer/interfaces@0.84.3': - resolution: {integrity: sha512-dk32ykoNojt0mvEaIW6Vli5MGTbQo58uy3Epj7ahCgTHmWOKuw/0G83f2UmFprRwFx689YTXG38I/vbpltEjzg==} - - '@glimmer/interfaces@0.87.1': - resolution: {integrity: sha512-2lbwLY4Bt9i2SvwT4hhY0TgEYKhOMQBgYvRiraq2BYHwO8iLKh3lC8iO3d+rQ3VgDtQ9i/sP6HG848VNRyVHxA==} - - '@glimmer/manager@0.87.1': - resolution: {integrity: sha512-jEUZZQWcuxKg+Ri/A1HGURm9pBrx13JDHx1djYCnPo96yjtQFYxEG0VcwLq2EtAEpFrekwfO1b6m3JZiFqmtGg==} - - '@glimmer/node@0.87.1': - resolution: {integrity: sha512-peESyArA08Va9f3gpBnhO+RNkK4Oe0Q8sMPQILCloAukNe2+DQOhTvDgVjRUKmVXMJCWoSgmJtxkiB3ZE193vw==} - - '@glimmer/opcode-compiler@0.87.1': - resolution: {integrity: sha512-D9OFrH3CrGNXfGtgcVWvu3xofpQZPoYFkqj3RrcDwnsSIYPSqUYTIOO6dwpxTbPlzkASidq0B2htXK7WkCERVw==} - - '@glimmer/owner@0.87.1': - resolution: {integrity: sha512-ayYjznPMSGpgygNT7XlTXeel6Cl/fafm4WJeRRgdPxG1EZMjKPlfpfAyNzf9peEIlW3WMbPu3RAIYrf54aThWQ==} - - '@glimmer/program@0.87.1': - resolution: {integrity: sha512-+r1Dz5Da0zyYwBhPmqoXiw3qmDamqqhVmSCtJLLcZ6exXXC2ZjGoNdynfos80A91dx+PFqYgHg+5lfa5STT9iQ==} - - '@glimmer/reference@0.87.1': - resolution: {integrity: sha512-KJwKYDnds6amsmVB1YxmFhJGI/TNCNmsFBWKUH8K0odmiggUCjt3AwUoOKztkwh3xxy/jpq+5AahIuV+uBgW7A==} - - '@glimmer/runtime@0.87.1': - resolution: {integrity: sha512-7QBONxRFesnHzelCiUahZjRj3nhbUxPg0F+iD+3rALrXaWfB1pkhngMTK2DYEmsJ7kq04qVzwBnTSrqsmLzOTg==} - - '@glimmer/syntax@0.84.3': - resolution: {integrity: sha512-ioVbTic6ZisLxqTgRBL2PCjYZTFIwobifCustrozRU2xGDiYvVIL0vt25h2c1ioDsX59UgVlDkIK4YTAQQSd2A==} - - '@glimmer/syntax@0.87.1': - resolution: {integrity: sha512-zYzZT6LgpSF0iv5iuxmMV5Pf52aE8dukNC2KfrHC6gXJfM4eLZMZcyk76NW5m+SEetZSOXX6AWv/KwLnoxiMfQ==} - - '@glimmer/tracking@1.1.2': - resolution: {integrity: sha512-cyV32zsHh+CnftuRX84ALZpd2rpbDrhLhJnTXn9W//QpqdRZ5rdMsxSY9fOsj0CKEc706tmEU299oNnDc0d7tA==} - - '@glimmer/util@0.44.0': - resolution: {integrity: sha512-duAsm30uVK9jSysElCbLyU6QQYO2X9iLDLBIBUcCqck9qN1o3tK2qWiHbGK5d6g8E2AJ4H88UrfElkyaJlGrwg==} - - '@glimmer/util@0.84.3': - resolution: {integrity: sha512-qFkh6s16ZSRuu2rfz3T4Wp0fylFj3HBsONGXQcrAdZjdUaIS6v3pNj6mecJ71qRgcym9Hbaq/7/fefIwECUiKw==} - - '@glimmer/util@0.87.1': - resolution: {integrity: sha512-Duxi2JutaIewfIWp8PJl7U5n12yasKWtZFHNLSrg+C8TKeMXdRyJtI7uqtqg0Z/6F9JwdJe/IPhTvdsTTfzAuA==} - - '@glimmer/validator@0.44.0': - resolution: {integrity: sha512-i01plR0EgFVz69GDrEuFgq1NheIjZcyTy3c7q+w7d096ddPVeVcRzU3LKaqCfovvLJ+6lJx40j45ecycASUUyw==} - - '@glimmer/validator@0.87.1': - resolution: {integrity: sha512-GqzULgK9m2QPfPswhyV30tZmsUegowv9Tyfz2l15cLDFX9L5GcEORpzKXjR0TzCplffuqOC1g8rnMaPsP55apw==} - - '@glimmer/vm-babel-plugins@0.84.2': - resolution: {integrity: sha512-HS2dEbJ3CgXn56wk/5QdudM7rE3vtNMvPIoG7Rrg+GhkGMNxBCIRxOeEF2g520j9rwlA2LAZFpc7MCDMFbTjNA==} - - '@glimmer/vm-babel-plugins@0.87.1': - resolution: {integrity: sha512-VbhYHa+HfGFiTIOOkvFuYPwBTaDvWTAR1Q55RI25JI6Nno0duBLB3UVRTDgHM+iOfbgRN7OSR5XCe/C5X5C5LA==} - engines: {node: '>=16'} - - '@glimmer/vm@0.87.1': - resolution: {integrity: sha512-JSFr85ASZmuN4H72px7GHtnW79PPRHpqHw6O/6UUZd+ocwWHy+nG9JGbo8kntvqN5xP0SdCipjv/c0u7nkc8tg==} - - '@glimmer/wire-format@0.87.1': - resolution: {integrity: sha512-O3W1HDfRGX7wHZqvP8UzI/nWyZ2GIMFolU7l6WcLGU9HIdzqfxsc7ae2Icob/fq2kV9meHti4yDEdTMlBVK9AQ==} - - '@gwhitney/detect-indent@7.0.1': - resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==} - engines: {node: '>=12.20'} - - '@handlebars/parser@2.0.0': - resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} - - '@humanwhocodes/config-array@0.11.8': - resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} - engines: {node: '>=10.10.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - - '@jridgewell/gen-mapping@0.1.1': - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.0': - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.1.2': - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.2': - resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} - - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - - '@jridgewell/trace-mapping@0.3.17': - resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': - resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} - - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@npmcli/fs@1.1.1': - resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} - - '@npmcli/move-file@1.1.2': - resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} - engines: {node: '>=10'} - deprecated: This functionality has been moved to @npmcli/fs - - '@pnpm/cli-meta@5.0.1': - resolution: {integrity: sha512-s7rVArn3s78w2ZDWC2/NzMaYBzq39QBmo1BQ4+qq1liX+ltSErDyAx3M/wvvJQgc+Ur3dZJYuc9t96roPnW3XQ==} - engines: {node: '>=16.14'} - - '@pnpm/cli-utils@2.0.9': - resolution: {integrity: sha512-mNujOPCopIi4r7D2HJ96hHKPEr/UPuZGruQvPVvjoc/pCP0l+y38xZAT72W2WhEM4Fo/zP8L+6g/zf88qUSbbg==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/config.env-replace@1.1.0': - resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} - engines: {node: '>=12.22.0'} - - '@pnpm/config@18.4.0': - resolution: {integrity: sha512-8B4Pw7cnMvO3kYUBZYYIjg6BcGhHwxEEkmBAcqAeF9NM6LmG6F0lFNsOf6XPfHZMx2vUTpZxaWo0FQo1uU2AAw==} - engines: {node: '>=16.14'} - - '@pnpm/constants@7.1.0': - resolution: {integrity: sha512-PzpiPtGF+bIrmkNaHgOIfBZw669+rkUtt/5UFzHukiETwI4/+BTYz8FAr+m5Dfuns531Y+fYRFOpB0PdbAU0+w==} - engines: {node: '>=16.14'} - - '@pnpm/constants@7.1.1': - resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} - engines: {node: '>=16.14'} - - '@pnpm/core-loggers@9.0.1': - resolution: {integrity: sha512-qP/kk6OeLSxqhvA4n6u4XB6evqD9h1w9p4qtdBOVbkZloCK7L9btkSmKNolBoQ3wrOz7WRFfjRekYUSKphMMCg==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/dedupe.issues-renderer@1.0.0': - resolution: {integrity: sha512-vlo2t1ERLH3vsL1PtlCue6qfpWofN2Pt2bvGIPtN6Y4siCZVwjy9GU3yXJk1wS2+a7qj9plPiobebadJgV/VHw==} - engines: {node: '>=16.14'} - - '@pnpm/dedupe.types@1.0.0': - resolution: {integrity: sha512-WGZ5E7aMPwaM+WMFYszTCP3Sms/gE0nLgI37gFnNbaKgAh5R7GojSHCxLgXqjiz0Jwx+Qi9BmdDgN1cJs5XBsg==} - engines: {node: '>=16.14'} - - '@pnpm/default-reporter@12.2.3': - resolution: {integrity: sha512-ALV6AQOcRPJ5bZlcCHDFQ4cEqH2B/2Luu0VYoAoofINgbhNDOKCrV6PkqLvnMQps98k1f7mtn4w/u4r99+qr7g==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/error@5.0.1': - resolution: {integrity: sha512-JQSOeSEqrV6k6+kKgrlSJ7gddJRcjxtNCxSVJRIqwckkGSdSTNpXmKEdGgLlaDuEwElPAZUmLDGSqk5InJ5pMA==} - engines: {node: '>=16.14'} - - '@pnpm/error@5.0.3': - resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} - engines: {node: '>=16.14'} - - '@pnpm/fetcher-base@14.0.1': - resolution: {integrity: sha512-DXPZ33CrmDQXnYzwvqyP7I0BF0MQELo4ah2JGpXhLhgOdzU+vj7zdKFo2x82L8anrK861IRi01V8o14oATq1vA==} - engines: {node: '>=16.14'} - - '@pnpm/find-workspace-dir@6.0.3': - resolution: {integrity: sha512-0iJnNkS4T8lJE4ldOhRERgER1o59iHA1nMlvpUI5lxNC9SUruH6peRUOlP4/rNcDg+UQ9u0rt5loYOnWKCojtw==} - engines: {node: '>=16.14'} - - '@pnpm/find-workspace-packages@6.0.9': - resolution: {integrity: sha512-80t6m6w3EfOg5k88CR8Eya6aOJi2uXyYGFSv2Y+3DqGAWD2x6CFLM3kop2Zi1nL9THMYpYF3hLnBRbqcJ8rmRg==} - engines: {node: '>=16.14'} - - '@pnpm/fs.find-packages@2.0.1': - resolution: {integrity: sha512-QxG4YrnqnFdi9zmGxzUUH7YF6hgFqtPjDmiMlUvPbASSFRIr6mIT1rTynos2cbg0bRGXpLpp+0XtyOMdDGnBnQ==} - engines: {node: '>=16.14'} - - '@pnpm/fs.hard-link-dir@2.0.1': - resolution: {integrity: sha512-ZsNyKG9YV9rZRhubj8bLxnysLg1LUwh0rdlVnp6ScIT9CtAA+C74kx2KK9E4TNofgb3qAbRqU4aKOaAoLmTSjg==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/git-utils@1.0.0': - resolution: {integrity: sha512-lUI+XrzOJN4zdPGOGnFUrmtXAXpXi8wD8OI0nWOZmlh+raqbLzC3VkXu1zgaduOK6YonOcnQW88O+ojav1rAdA==} - engines: {node: '>=16.14'} - - '@pnpm/graceful-fs@3.0.0': - resolution: {integrity: sha512-72kkqIL2sacOVr6Y6B6xDGjRC4QgTLeIGkw/5XYyeMgMeL9mDE0lonZEOL9JuLS0XPOXQoyDtRCSmUrzAA57LQ==} - engines: {node: '>=16.14'} - - '@pnpm/graceful-fs@3.2.0': - resolution: {integrity: sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==} - engines: {node: '>=16.14'} - - '@pnpm/hooks.types@1.0.1': - resolution: {integrity: sha512-Zx2hzwxBKv1RmFzyu4pEVY7QeIGUb54smSSYt8GcJgByn+uMXgwJ7ydv9t2Koc90QTqk8J3P2J+RDrZVIQpVQw==} - engines: {node: '>=16.14'} - - '@pnpm/lockfile-types@5.1.0': - resolution: {integrity: sha512-14eYp9iOdJ7SyOIVXomXhbVnc14DEhzMLS3eKqxYxi9LkANUfxx1/pwRiRY/lTiP9RFS+OkIcTm2QiLsmNEctw==} - engines: {node: '>=16.14'} - - '@pnpm/logger@5.0.0': - resolution: {integrity: sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==} - engines: {node: '>=12.17'} - - '@pnpm/manifest-utils@5.0.1': - resolution: {integrity: sha512-vQUmd0NQNv1yWEeFA4pjuBCs4AqhaHW4bVpuaD19lHE5J9SCs7iNRDpjnxjTm/qgDgO/hqu/spuAXEbPxR8u0A==} - engines: {node: '>=16.14'} - - '@pnpm/matcher@5.0.0': - resolution: {integrity: sha512-uh+JBmW8XHGwz9x0K0Ok+TtMiu3ghEaqHHm7dqIubitBP8y9Y0LLP6D2fxWblogjpVzSlH3DpDR1Vicuhw9/cQ==} - engines: {node: '>=16.14'} - - '@pnpm/network.ca-file@1.0.2': - resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} - engines: {node: '>=12.22.0'} - - '@pnpm/npm-conf@2.2.0': - resolution: {integrity: sha512-roLI1ul/GwzwcfcVpZYPdrgW2W/drLriObl1h+yLF5syc8/5ULWw2ALbCHUWF+4YltIqA3xFSbG4IwyJz37e9g==} - engines: {node: '>=12'} - - '@pnpm/package-is-installable@8.0.2': - resolution: {integrity: sha512-eYuqNBjzYf5wXbD4Xm6ZupRPjYxn2sp6mtYL9+bMntx1+yoUlCJABrYcSvbTM7kheoHyHRf+gEQDFKdn5trQ6w==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/pnpmfile@5.0.7': - resolution: {integrity: sha512-A8uwamvs9jhf3DYLuGHCngWW8WXEDgcm3nwOeRTWJOOgButgXueIRHcEZPiKgQwy6t116ntimNeW5H3/hjim6w==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - - '@pnpm/ramda@0.28.1': - resolution: {integrity: sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==} - - '@pnpm/read-project-manifest@5.0.1': - resolution: {integrity: sha512-MDXuQpYFbabSXzAnqP7VIQqBx5Z1fzOhzB/3YmIXJ+tE7Wue//IR3itMSYlWeaFLo1G5PCJklM2zBdvggRw1nw==} - engines: {node: '>=16.14'} - - '@pnpm/read-project-manifest@5.0.11': - resolution: {integrity: sha512-themRLiDt9Ud6Somlu0PJbeprBBQEhlI1xNG5bZIv26yfLsc1vYLd1TfgGViD1b8fP0jxAqsUrDM+WMaMKI+gw==} - engines: {node: '>=16.14'} - - '@pnpm/render-peer-issues@4.0.1': - resolution: {integrity: sha512-+SsNmbBHH7lBsFrs6dQCEWRtT+Bmq9MYxu+xgkXRplyvjSEQmM0h/UduIw5s8ZAlUuQcxNVTvl0b7ul6OPEIwg==} - engines: {node: '>=16.14'} - - '@pnpm/resolver-base@10.0.1': - resolution: {integrity: sha512-2yufLOpiPKQyNVLbL3dgoytkDuuURB5yBOrFtafiuZieGZJid2AeHmFfPhU9hNc/ZM1+wqH3EuVHe/1DdEgm4Q==} - engines: {node: '>=16.14'} - - '@pnpm/store-controller-types@15.0.1': - resolution: {integrity: sha512-S88sR6xhQ1ZDhMRIjhaRBA11N2OIDU2W+60szQLU8e2bw+KgGU60LbcXMunTdRnJskuB9UfDyoN6YuRtETBqYA==} - engines: {node: '>=16.14'} - - '@pnpm/text.comments-parser@2.0.0': - resolution: {integrity: sha512-DRWtTmmxQQtuWHf1xPt9bqzCSq8d0MQF5x1kdpCDMLd7xk3nP4To2/OGkPrb8MKbrWsgCNDwXyKCFlEKrAg7fg==} - engines: {node: '>=16.14'} - - '@pnpm/types@9.1.0': - resolution: {integrity: sha512-MMPDMLOY17bfNhLhR9Qmq6/2keoocnR5DWXZfZDC4dKXugrMsE1jB6RnuU8swJIo4zyCsMT/iVSAtl/XK+9Z+A==} - engines: {node: '>=16.14'} - - '@pnpm/types@9.4.2': - resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} - engines: {node: '>=16.14'} - - '@pnpm/util.lex-comparator@1.0.0': - resolution: {integrity: sha512-3aBQPHntVgk5AweBWZn+1I/fqZ9krK/w01197aYVkAJQGftb+BVWgEepxY5GChjSW12j52XX+CmfynYZ/p0DFQ==} - engines: {node: '>=12.22.0'} - - '@pnpm/write-project-manifest@5.0.1': - resolution: {integrity: sha512-zU4vDfBUx/jUBPmR4CzCqPDOPObb/7iLT3UZvhXSJ8ZXDo9214V6agnJvxQ6bYBcypdiKva0hnb3tmo1chQBYg==} - engines: {node: '>=16.14'} - - '@pnpm/write-project-manifest@5.0.6': - resolution: {integrity: sha512-3qkKCftRE/HXzoWedyDuaMMUQzheDwx8AQXR0DnA9ylsBnZQYNut19Ado/gzi5+IvznaMcqrBszw57j3y1/ILw==} - engines: {node: '>=16.14'} - - '@rollup/plugin-babel@6.0.3': - resolution: {integrity: sha512-fKImZKppa1A/gX73eg4JGo+8kQr/q1HBQaCGKECZ0v4YBBv3lFqi14+7xyApECzvkLTHCifx+7ntcrvtBIRcpg==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0||^3.0.0 - peerDependenciesMeta: - '@types/babel__core': - optional: true - rollup: - optional: true - - '@rollup/plugin-babel@6.0.4': - resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - '@types/babel__core': - optional: true - rollup: - optional: true - - '@rollup/plugin-node-resolve@15.0.1': - resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@15.2.3': - resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@4.2.1': - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - - '@rollup/pluginutils@5.0.2': - resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.17.2': - resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.17.2': - resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.17.2': - resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.17.2': - resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': - resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.17.2': - resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.17.2': - resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.17.2': - resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': - resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.17.2': - resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.17.2': - resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.17.2': - resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.17.2': - resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.17.2': - resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.17.2': - resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.17.2': - resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} - cpu: [x64] - os: [win32] - - '@simple-dom/document@1.4.0': - resolution: {integrity: sha512-/RUeVH4kuD3rzo5/91+h4Z1meLSLP66eXqpVAw/4aZmYozkeqUkMprq0znL4psX/adEed5cBgiNJcfMz/eKZLg==} - - '@simple-dom/interface@1.4.0': - resolution: {integrity: sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==} - - '@simple-dom/parser@1.4.0': - resolution: {integrity: sha512-TNjDkOehueRIKr1df416qk9ELj+qWuVVJNIT25y1aZg3pQvxv4UPGrgaDFte7dsWBTbF3V8NYPNQ5FDUZQ8Wlg==} - - '@simple-dom/serializer@1.4.0': - resolution: {integrity: sha512-mI1yRahsVs8atXLiQksineDsFEFqeG7RHwnnBTDOK6inbzl4tZQgjR+Z7edjgIJq5j5RhZvwPI6EuCji9B3eQw==} - - '@simple-dom/void-map@1.4.0': - resolution: {integrity: sha512-VDhLEyVCbuhOBBgHol9ShzIv9O8UCzdXeH4FoXu2DOcu/nnvTjLTck+BgXsCLv5ynDiUdoqsREEVFnoyPpFKVw==} - - '@sindresorhus/is@0.14.0': - resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} - engines: {node: '>=6'} - - '@socket.io/component-emitter@3.1.0': - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} - - '@szmarczak/http-timer@1.1.2': - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} - - '@tootallnate/once@1.1.2': - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} - - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - - '@types/babel__code-frame@7.0.3': - resolution: {integrity: sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==} - - '@types/body-parser@1.19.2': - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - - '@types/broccoli-plugin@3.0.0': - resolution: {integrity: sha512-f+TcsARR2PovfFRKFdCX0kfH/QoM3ZVD2h1rl2mNvrKO0fq2uBNCBsTU3JanfU4COCt5cXpTfARyUsERlC8vIw==} - deprecated: This is a stub types definition. broccoli-plugin provides its own type definitions, so you do not need this installed. - - '@types/chai-as-promised@7.1.5': - resolution: {integrity: sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==} - - '@types/chai@4.3.4': - resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} - - '@types/connect@3.4.35': - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - - '@types/cookie@0.4.1': - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - - '@types/cors@2.8.13': - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} - - '@types/ember-qunit@5.0.2': - resolution: {integrity: sha512-LXp0Ew4wPhaCMuw49cNDHWs/bROn+Msb4ypMG1t60EMBx0UaEoX0tZlkf5bQJfdD249WZTu5cCtKvl47xbQzxA==} - - '@types/ember-resolver@5.0.13': - resolution: {integrity: sha512-pO964cAPhAaFJoS28M8+b5MzAhQ/tVuNM4GDUIAexheQat36axG2WTG8LQ5ea07MSFPesrRFk2T3z88pfvdYKA==} - - '@types/ember-testing-helpers@0.0.4': - resolution: {integrity: sha512-6EEY+kk4+HsKMzLkzZp0UU7TzUG1EB2mPyORrQXcudjJ0M7k67Z9cCBDn7kupDcu4NVgtG7HNRZTZgBljOjxoA==} - - '@types/ember@4.0.3': - resolution: {integrity: sha512-lRhIsa05KxPctv2mhVS/3lOwM8xnppEDsZu595Y+lE3IJhmhnXTjl3Ek+HMOPf53We2DFps+YeXSLm/UFiCILQ==} - - '@types/ember__application@4.0.5': - resolution: {integrity: sha512-qnU1RFZ3oIfw7ncLSjYqe1p236SU5OMQQVPaXISpNcVr4IEAl6yZ6Txm8pxI7DKo7isHV8sHssPBara9oqccVA==} - - '@types/ember__array@4.0.3': - resolution: {integrity: sha512-G6kbLaS3ke4QspHkgLlGY0t1v0G22hGavyphezZucj7LLk1N+r11w913CYkBg3cJsJD+TG2Wo4eVbgRcotvuvQ==} - - '@types/ember__component@4.0.12': - resolution: {integrity: sha512-qHjCGo1p9I4VJR3qfil7h0jJWTy52uNJw87MNwNEi1SYk+EaVJaqyyyoZHJmZGdn8hw3JjXvyFt/zeFZpbK/6A==} - - '@types/ember__controller@4.0.4': - resolution: {integrity: sha512-+f0knTIJJkRX5xijeSI/n4FvLfhMFFxIxODyFFFFB483EryYuts3QzpTwU5D66WQ5rAbZvpPRXRMPTTCNJoUhg==} - - '@types/ember__debug@4.0.3': - resolution: {integrity: sha512-LvSLFgNlzpbsdb479ohS2szCFwkAsaqPnTjyPML7xFF3r3VGFMQjVNTXQpFYQCKTMAC1FYRX1N6hw/8lpXWHKA==} - - '@types/ember__engine@4.0.4': - resolution: {integrity: sha512-dxQf3ESRjTJtCHbd42/ReUpQUAUsn/VtI6+S07jrsgCbAQEr8Qkh/dJpd9Cta8N+DpbY1CUH58D4HxdOC4Ip3A==} - - '@types/ember__error@4.0.2': - resolution: {integrity: sha512-0KVIvGrpyYzO4dmBm04ovJ/Fd7DjiXABxkKX42O8U01OL6O+Q+m3euQuJbB5wkYVANnvBHpcHlxRUI2y9KmzYg==} - - '@types/ember__object@4.0.5': - resolution: {integrity: sha512-gXrywWBwoW7J9y9yJqoZ0m1qtiyMdrEi29cJdF1xI2qOnMqaZeuSCMYaPQMsyq52/YnVIG2EnGzo6eUD57J4Nw==} - - '@types/ember__owner@4.0.3': - resolution: {integrity: sha512-vwVKdLNYKXMOxJXwMnFQh7TkWfkp6berH6Kc/VK8is1bPgaBB7X/c50rjNmM2o7zI5LJSPm1khxcDidfyXnimg==} - - '@types/ember__polyfills@4.0.1': - resolution: {integrity: sha512-IT3oovEPxLiaNCcPqY5hdVlgiRaMT8gIIrJodFt5MDEashCZDYJMn2XlqUtTXcYIFaume32PbbTBCxuhd3rhHA==} - - '@types/ember__routing@4.0.12': - resolution: {integrity: sha512-zxPS43JP8/dEmNrSucN5KzTvOm+JUrbFGWsJ1m5a395FwxYbpgs7JujV0JWl+eVhnCh/PmsNcCdJT16+jouktQ==} - - '@types/ember__runloop@4.0.2': - resolution: {integrity: sha512-E0/n/O/JnPQpMrabsDKtVOXX4tbCrOA116HjmD+eorgsPFLm8tAUwl3wQGroeJt8BSE7uHjsQdDA7JUkbsT3IQ==} - - '@types/ember__service@4.0.2': - resolution: {integrity: sha512-7SCTMEexxOdkpkgdyf1QLFQJhoAq6aqP6dPH9fcG8N5mTMvZGLMNIKGG9bldiq3NzHS9Pxogu3qgo5yMfc2+jA==} - - '@types/ember__string@3.0.10': - resolution: {integrity: sha512-dxbW06IqPdieA4SEyUfyCUnL8iqUnzdcLUtrfkf8g+DSP2K/RGiexfG6w2NOyOetq8gw8F/WUpNYfMmBcB6Smw==} - - '@types/ember__string@3.0.15': - resolution: {integrity: sha512-SxoaweAJUJKSIt82clIwpi/Fm0IfeisozLnXthnBp/hE2JyVcnOb1wMIbw0CCfzercmyWG1njV45VBqy8SrLDQ==} - - '@types/ember__template@4.0.1': - resolution: {integrity: sha512-hAxzdJa0zNvZSoHoCbtd0KGt6Dls4Aph9EwdtbUcdnlMiSUtEDUdKTtDbUrysqJjxGBr4vWIdJEqWtZ0/Y8KBw==} - - '@types/ember__test-helpers@2.9.1': - resolution: {integrity: sha512-KZ6jYr0ZiQHlklLcfyuy1j+FkvygEayf5mZ9FcaSOahw9ghK3K5TYSRDCuIBELRw//OB/WP9J7v9NduFRNfRgg==} - deprecated: This is a stub types definition. @ember/test-helpers provides its own type definitions, so you do not need this installed. - - '@types/ember__test@4.0.1': - resolution: {integrity: sha512-EXFbZcROB9mUNHiDRyhyoJGXRIzxgo++smS3/kmmDlhM8/pIdULLKJSelTcFOy3e/VuZhf8y8ZCJLXKP74oCBQ==} - - '@types/ember__utils@4.0.2': - resolution: {integrity: sha512-LWkLgf09/GqyrUuoKtAB6qP7n36yAzc2yOh1L5fVpZGCBv5KQiGWUQv5uBoo4c1mllD+IBOMxei3bR4cx6SwZA==} - - '@types/eslint-scope@3.7.4': - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} - - '@types/eslint@8.21.3': - resolution: {integrity: sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw==} - - '@types/estree@0.0.51': - resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} - - '@types/estree@1.0.0': - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - - '@types/express-serve-static-core@4.17.33': - resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} - - '@types/express@4.17.17': - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} - - '@types/fs-extra@5.1.0': - resolution: {integrity: sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==} - - '@types/fs-extra@8.1.2': - resolution: {integrity: sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==} - - '@types/glob@7.2.0': - resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - - '@types/htmlbars-inline-precompile@3.0.0': - resolution: {integrity: sha512-n1YwM/Q937KmS9W4Ytran71nzhhcT2FDQI00eRGBNUyeErLZspBdDBewEe1F8tcRlUdsCVo2AZBLJsRjEceTRg==} - - '@types/jquery@3.5.16': - resolution: {integrity: sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==} - - '@types/json-schema@7.0.11': - resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/mime@3.0.1': - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - - '@types/minimatch@3.0.5': - resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - - '@types/node@18.15.10': - resolution: {integrity: sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==} - - '@types/qs@6.9.7': - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - - '@types/qunit@2.19.4': - resolution: {integrity: sha512-EocRiD2JRWrOaA0dnyyLX083DIo1p3OSBBiGODcHaMzOFhteXtvRRp0kKsiYYqynnBSMqnqRI92iE32axdoXZw==} - - '@types/range-parser@1.2.4': - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/responselike@1.0.0': - resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} - - '@types/rimraf@2.0.5': - resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} - - '@types/rsvp@4.0.4': - resolution: {integrity: sha512-J3Ol++HCC7/hwZhanDvggFYU/GtxHxE/e7cGRWxR04BF7Tt3TqJZ84BkzQgDxmX0uu8IagiyfmfoUlBACh2Ilg==} - - '@types/semver@7.3.13': - resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} - - '@types/serve-static@1.15.1': - resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} - - '@types/sizzle@2.3.3': - resolution: {integrity: sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==} - - '@types/source-map@0.5.7': - resolution: {integrity: sha512-LrnsgZIfJaysFkv9rRJp4/uAyqw87oVed3s1hhF83nwbo9c7MG9g5DqR0seHP+lkX4ldmMrVolPjQSe2ZfD0yA==} - deprecated: This is a stub types definition for source-map (https://github.com/mozilla/source-map). source-map provides its own type definitions, so you don't need @types/source-map installed! - - '@types/ssri@7.1.5': - resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==} - - '@types/supports-color@8.1.1': - resolution: {integrity: sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw==} - - '@types/symlink-or-copy@1.2.0': - resolution: {integrity: sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==} - - '@types/tmp@0.0.33': - resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} - - '@types/yargs-parser@21.0.0': - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - - '@types/yargs@17.0.23': - resolution: {integrity: sha512-yuogunc04OnzGQCrfHx+Kk883Q4X0aSwmYZhKjI21m+SVYzjIbrWl8dOOwSv5hf2Um2pdCOXWo9isteZTNXUZQ==} - - '@typescript-eslint/eslint-plugin@5.57.1': - resolution: {integrity: sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@5.57.1': - resolution: {integrity: sha512-hlA0BLeVSA/wBPKdPGxoVr9Pp6GutGoY380FEhbVi0Ph4WNe8kLvqIRx76RSQt1lynZKfrXKs0/XeEk4zZycuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@5.57.1': - resolution: {integrity: sha512-N/RrBwEUKMIYxSKl0oDK5sFVHd6VI7p9K5MyUlVYAY6dyNb/wHUqndkTd3XhpGlXgnQsBkRZuu4f9kAHghvgPw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/type-utils@5.57.1': - resolution: {integrity: sha512-/RIPQyx60Pt6ga86hKXesXkJ2WOS4UemFrmmq/7eOyiYjYv/MUSHPlkhU6k9T9W1ytnTJueqASW+wOmW4KrViw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@5.57.1': - resolution: {integrity: sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/typescript-estree@5.57.1': - resolution: {integrity: sha512-A2MZqD8gNT0qHKbk2wRspg7cHbCDCk2tcqt6ScCFLr5Ru8cn+TCfM786DjPhqwseiS+PrYwcXht5ztpEQ6TFTw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@5.57.1': - resolution: {integrity: sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - - '@typescript-eslint/visitor-keys@5.57.1': - resolution: {integrity: sha512-RjQrAniDU0CEk5r7iphkm731zKlFiUjvcBS2yHAg8WWqFMCaCrD0rKEVOMUyMMcbGPZ0bPp56srkGWrgfZqLRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@webassemblyjs/ast@1.11.1': - resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} - - '@webassemblyjs/floating-point-hex-parser@1.11.1': - resolution: {integrity: sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==} - - '@webassemblyjs/helper-api-error@1.11.1': - resolution: {integrity: sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==} - - '@webassemblyjs/helper-buffer@1.11.1': - resolution: {integrity: sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==} - - '@webassemblyjs/helper-numbers@1.11.1': - resolution: {integrity: sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==} - - '@webassemblyjs/helper-wasm-bytecode@1.11.1': - resolution: {integrity: sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==} - - '@webassemblyjs/helper-wasm-section@1.11.1': - resolution: {integrity: sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==} - - '@webassemblyjs/ieee754@1.11.1': - resolution: {integrity: sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==} - - '@webassemblyjs/leb128@1.11.1': - resolution: {integrity: sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==} - - '@webassemblyjs/utf8@1.11.1': - resolution: {integrity: sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==} - - '@webassemblyjs/wasm-edit@1.11.1': - resolution: {integrity: sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==} - - '@webassemblyjs/wasm-gen@1.11.1': - resolution: {integrity: sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==} - - '@webassemblyjs/wasm-opt@1.11.1': - resolution: {integrity: sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==} - - '@webassemblyjs/wasm-parser@1.11.1': - resolution: {integrity: sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==} - - '@webassemblyjs/wast-printer@1.11.1': - resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==} - - '@xmldom/xmldom@0.8.6': - resolution: {integrity: sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==} - engines: {node: '>=10.0.0'} - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - '@zkochan/which@2.0.3': - resolution: {integrity: sha512-C1ReN7vt2/2O0fyTsx5xnbQuxBrmG5NMSbcIkPKCCfCTJgpZBsuRYzFXHj3nVq8vTfK7vxHUmzfCpSHgO7j4rg==} - engines: {node: '>= 8'} - hasBin: true - - abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - acorn-globals@6.0.0: - resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} - - acorn-import-assertions@1.8.0: - resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} - peerDependencies: - acorn: ^8 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - - agentkeepalive@4.3.0: - resolution: {integrity: sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==} - engines: {node: '>= 8.0.0'} - - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - - ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 - - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - - amd-name-resolver@1.3.1: - resolution: {integrity: sha512-26qTEWqZQ+cxSYygZ4Cf8tsjDBLceJahhtewxtKZA3SRa4PluuqYCuheemDQD+7Mf5B7sr+zhTDWAHDh02a1Dw==} - engines: {node: 6.* || 8.* || >= 10.*} - - amdefine@1.0.1: - resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} - engines: {node: '>=0.4.2'} - - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - - ansi-colors@4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} - engines: {node: '>=6'} - - ansi-diff@1.1.1: - resolution: {integrity: sha512-XnTdFDQzbEewrDx8epWXdw7oqHMvv315vEtfqDiEhhWghIf4++h26c3/FMz7iTLhNrnj56DNIXpbxHZq+3s6qw==} - - ansi-escapes@3.2.0: - resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} - engines: {node: '>=4'} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - - ansi-html@0.0.7: - resolution: {integrity: sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==} - engines: {'0': node >= 0.8.0} - hasBin: true - - ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - - ansi-regex@3.0.1: - resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} - engines: {node: '>=4'} - - ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-split@1.0.1: - resolution: {integrity: sha512-RRxQym4DFtDNmHIkW6aeFVvrXURb11lGAEPXNiryjCe8bK8RsANjzJ0M2aGOkvBYwP4Bl/xZ8ijtr6D3j1x/eg==} - - ansi-styles@2.2.1: - resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} - engines: {node: '>=0.10.0'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-to-html@0.6.15: - resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==} - engines: {node: '>=8.0.0'} - hasBin: true - - ansicolors@0.2.1: - resolution: {integrity: sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@2.0.0: - resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - - archy@1.0.0: - resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} - - are-we-there-yet@3.0.1: - resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - arr-diff@4.0.0: - resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} - engines: {node: '>=0.10.0'} - - arr-flatten@1.1.0: - resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} - engines: {node: '>=0.10.0'} - - arr-union@3.1.0: - resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} - engines: {node: '>=0.10.0'} - - array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - - array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} - - array-equal@1.0.0: - resolution: {integrity: sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA==} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - array-includes@3.1.6: - resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} - engines: {node: '>= 0.4'} - - array-to-error@1.1.1: - resolution: {integrity: sha512-kqcQ8s7uQfg3UViYON3kCMcck3A9exxgq+riVuKy08Mx00VN4EJhK30L2VpjE58LQHKhcE/GRpvbVUhqTvqzGQ==} - - array-to-sentence@1.1.0: - resolution: {integrity: sha512-YkwkMmPA2+GSGvXj1s9NZ6cc2LBtR+uSeWTy2IGi5MR1Wag4DdrcjTxA/YV/Fw+qKlBeXomneZgThEbm/wvZbw==} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array-unique@0.3.2: - resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} - engines: {node: '>=0.10.0'} - - array.prototype.flat@1.3.1: - resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.1: - resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} - engines: {node: '>= 0.4'} - - as-table@1.0.55: - resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} - - asn1@0.1.11: - resolution: {integrity: sha512-Fh9zh3G2mZ8qM/kwsiKwL2U2FmXxVsboP4x1mXjnhKHv3SmzaBZoYvxEQJz/YS2gnCgd8xlAVWcZnQyC9qZBsA==} - engines: {node: '>=0.4.9'} - - assert-never@1.2.1: - resolution: {integrity: sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==} - - assert-plus@0.1.5: - resolution: {integrity: sha512-brU24g7ryhRwGCI2y+1dGQmQXiZF7TtIj583S96y0jjdajIe6wn8BuXyELYhvD22dtIxDQVFk04YTJwwdwOYJw==} - engines: {node: '>=0.8'} - - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - - assign-symbols@1.0.0: - resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} - engines: {node: '>=0.10.0'} - - ast-types@0.13.3: - resolution: {integrity: sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==} - engines: {node: '>=4'} - - async-disk-cache@1.3.5: - resolution: {integrity: sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ==} - - async-disk-cache@2.1.0: - resolution: {integrity: sha512-iH+boep2xivfD9wMaZWkywYIURSmsL96d6MoqrC94BnGSvXE4Quf8hnJiHGFYhw/nLeIa1XyRaf4vvcvkwAefg==} - engines: {node: 8.* || >= 10.*} - - async-promise-queue@1.0.5: - resolution: {integrity: sha512-xi0aQ1rrjPWYmqbwr18rrSKbSaXIeIwSd1J4KAgVfkq8utNbdZoht7GfvfY6swFUAMJ9obkc4WPJmtGwl+B8dw==} - - async@0.2.10: - resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - - async@0.9.2: - resolution: {integrity: sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==} - - async@2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - atob@2.1.2: - resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} - engines: {node: '>= 4.5.0'} - hasBin: true - - available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - - aws-sign2@0.5.0: - resolution: {integrity: sha512-oqUX0DM5j7aPWPCnpWebiyNIj2wiNI87ZxnOMoGv0aE4TGlBy2N+5iWc6dQ/NOKZaBD2W6PVz8jtOGkWzSC5EA==} - - babel-code-frame@6.26.0: - resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} - - babel-helper-builder-binary-assignment-operator-visitor@6.24.1: - resolution: {integrity: sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==} - - babel-helper-call-delegate@6.24.1: - resolution: {integrity: sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==} - - babel-helper-define-map@6.26.0: - resolution: {integrity: sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==} - - babel-helper-explode-assignable-expression@6.24.1: - resolution: {integrity: sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==} - - babel-helper-function-name@6.24.1: - resolution: {integrity: sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==} - - babel-helper-get-function-arity@6.24.1: - resolution: {integrity: sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==} - - babel-helper-hoist-variables@6.24.1: - resolution: {integrity: sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==} - - babel-helper-optimise-call-expression@6.24.1: - resolution: {integrity: sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==} - - babel-helper-regex@6.26.0: - resolution: {integrity: sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==} - - babel-helper-remap-async-to-generator@6.24.1: - resolution: {integrity: sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==} - - babel-helper-replace-supers@6.24.1: - resolution: {integrity: sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==} - - babel-import-util@0.2.0: - resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} - engines: {node: '>= 12.*'} - - babel-import-util@1.3.0: - resolution: {integrity: sha512-PPzUT17eAI18zn6ek1R3sB4Krc/MbnmT1MkZQFmyhjoaEGBVwNABhfVU9+EKcDSKrrOm9OIpGhjxukx1GCiy1g==} - engines: {node: '>= 12.*'} - - babel-import-util@2.1.1: - resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} - engines: {node: '>= 12.*'} - - babel-loader@8.3.0: - resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} - engines: {node: '>= 8.9'} - peerDependencies: - '@babel/core': ^7.0.0 - webpack: '>=2' - - babel-messages@6.23.0: - resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} - - babel-plugin-check-es2015-constants@6.22.0: - resolution: {integrity: sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==} - - babel-plugin-debug-macros@0.2.0: - resolution: {integrity: sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-beta.42 - - babel-plugin-debug-macros@0.3.4: - resolution: {integrity: sha512-wfel/vb3pXfwIDZUrkoDrn5FHmlWI96PCJ3UCDv2a86poJ3EQrnArNW5KfHSVJ9IOgxHbo748cQt7sDU+0KCEw==} - engines: {node: '>=6'} - peerDependencies: - '@babel/core': ^7.0.0 - - babel-plugin-ember-data-packages-polyfill@0.1.2: - resolution: {integrity: sha512-kTHnOwoOXfPXi00Z8yAgyD64+jdSXk3pknnS7NlqnCKAU6YDkXZ4Y7irl66kaZjZn0FBBt0P4YOZFZk85jYOww==} - engines: {node: 6.* || 8.* || 10.* || >= 12.*} - - babel-plugin-ember-modules-api-polyfill@3.5.0: - resolution: {integrity: sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA==} - engines: {node: 6.* || 8.* || >= 10.*} - - babel-plugin-ember-template-compilation@2.0.0: - resolution: {integrity: sha512-d+4jaB2ik0rt9TH0K9kOlKJeRBHEb373FgFMcU9ZaJL2zYuVXe19bqy+cWlLpLf1tpOBcBG9QTlFBCoImlOt1g==} - engines: {node: '>= 12.*'} - - babel-plugin-ember-template-compilation@2.2.2: - resolution: {integrity: sha512-wdT2F9/n6uC1rLvAjXCx5+fXbwkl8MIcwt0rg5csWedPbERdzQqhRlDqj0kIwNfUJ9gaXAcKrgSOUXbJcByGOQ==} - engines: {node: '>= 12.*'} - - babel-plugin-filter-imports@4.0.0: - resolution: {integrity: sha512-jDLlxI8QnfKd7PtieH6pl4tZJzymzfCDCPGdTq/grgbiYAikwDPp/oL0IlFJn0HQjLpcLkyYhPKkUVneRESw5w==} - engines: {node: '>=8'} - - babel-plugin-htmlbars-inline-precompile@5.3.1: - resolution: {integrity: sha512-QWjjFgSKtSRIcsBhJmEwS2laIdrA6na8HAlc/pEAhjHgQsah/gMiBFRZvbQTy//hWxR4BMwV7/Mya7q5H8uHeA==} - engines: {node: 10.* || >= 12.*} - - babel-plugin-module-resolver@3.2.0: - resolution: {integrity: sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA==} - engines: {node: '>= 6.0.0'} - - babel-plugin-module-resolver@4.1.0: - resolution: {integrity: sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==} - engines: {node: '>= 8.0.0'} - - babel-plugin-polyfill-corejs2@0.3.3: - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-corejs2@0.4.11: - resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.10.4: - resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.6.0: - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-regenerator@0.4.1: - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-regenerator@0.6.2: - resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-syntax-async-functions@6.13.0: - resolution: {integrity: sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==} - - babel-plugin-syntax-dynamic-import@6.18.0: - resolution: {integrity: sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==} - - babel-plugin-syntax-exponentiation-operator@6.13.0: - resolution: {integrity: sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==} - - babel-plugin-syntax-trailing-function-commas@6.22.0: - resolution: {integrity: sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==} - - babel-plugin-transform-async-to-generator@6.24.1: - resolution: {integrity: sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==} - - babel-plugin-transform-es2015-arrow-functions@6.22.0: - resolution: {integrity: sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==} - - babel-plugin-transform-es2015-block-scoped-functions@6.22.0: - resolution: {integrity: sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==} - - babel-plugin-transform-es2015-block-scoping@6.26.0: - resolution: {integrity: sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==} - - babel-plugin-transform-es2015-classes@6.24.1: - resolution: {integrity: sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==} - - babel-plugin-transform-es2015-computed-properties@6.24.1: - resolution: {integrity: sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==} - - babel-plugin-transform-es2015-destructuring@6.23.0: - resolution: {integrity: sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==} - - babel-plugin-transform-es2015-duplicate-keys@6.24.1: - resolution: {integrity: sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==} - - babel-plugin-transform-es2015-for-of@6.23.0: - resolution: {integrity: sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==} - - babel-plugin-transform-es2015-function-name@6.24.1: - resolution: {integrity: sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==} - - babel-plugin-transform-es2015-literals@6.22.0: - resolution: {integrity: sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==} - - babel-plugin-transform-es2015-modules-amd@6.24.1: - resolution: {integrity: sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==} - - babel-plugin-transform-es2015-modules-commonjs@6.26.2: - resolution: {integrity: sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==} - - babel-plugin-transform-es2015-modules-systemjs@6.24.1: - resolution: {integrity: sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==} - - babel-plugin-transform-es2015-modules-umd@6.24.1: - resolution: {integrity: sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==} - - babel-plugin-transform-es2015-object-super@6.24.1: - resolution: {integrity: sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==} - - babel-plugin-transform-es2015-parameters@6.24.1: - resolution: {integrity: sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==} - - babel-plugin-transform-es2015-shorthand-properties@6.24.1: - resolution: {integrity: sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==} - - babel-plugin-transform-es2015-spread@6.22.0: - resolution: {integrity: sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==} - - babel-plugin-transform-es2015-sticky-regex@6.24.1: - resolution: {integrity: sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==} - - babel-plugin-transform-es2015-template-literals@6.22.0: - resolution: {integrity: sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==} - - babel-plugin-transform-es2015-typeof-symbol@6.23.0: - resolution: {integrity: sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==} - - babel-plugin-transform-es2015-unicode-regex@6.24.1: - resolution: {integrity: sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==} - - babel-plugin-transform-exponentiation-operator@6.24.1: - resolution: {integrity: sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==} - - babel-plugin-transform-regenerator@6.26.0: - resolution: {integrity: sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==} - - babel-plugin-transform-strict-mode@6.24.1: - resolution: {integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==} - - babel-preset-env@1.7.0: - resolution: {integrity: sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==} - - babel-runtime@6.26.0: - resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} - - babel-template@6.26.0: - resolution: {integrity: sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==} - - babel-traverse@6.26.0: - resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} - - babel-types@6.26.0: - resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} - - babel6-plugin-strip-class-callcheck@6.0.0: - resolution: {integrity: sha512-biNFJ7JAK4+9BwswDGL0dmYpvXHvswOFR/iKg3Q/f+pNxPEa5bWZkLHI1fW4spPytkHGMe7f/XtYyhzml9hiWg==} - - babylon@6.18.0: - resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} - hasBin: true - - backbone@1.4.1: - resolution: {integrity: sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==} - - backburner.js@2.8.0: - resolution: {integrity: sha512-zYXY0KvpD7/CWeOLF576mV8S+bQsaIoj/GNLXXB+Eb8SJcQy5lqSjkRrZ0MZhdKUs9QoqmGNIEIe3NQfGiiscQ==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - base64id@2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - - base@0.11.2: - resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} - engines: {node: '>=0.10.0'} - - basic-auth@2.0.1: - resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} - engines: {node: '>= 0.8'} - - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - - binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - - binaryextensions@2.3.0: - resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==} - engines: {node: '>=0.8'} - - bind-decorator@1.0.11: - resolution: {integrity: sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - - blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} - - body-parser@1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - body@5.1.0: - resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} - - bole@5.0.12: - resolution: {integrity: sha512-G5H5siOlUrcyvYr7kVlQyYMWip0dZ8qa+Uiy+d9QxOvBY2eaP/g8YsJVwvf3VIMbXmYxZIAOmmsuN3rL5r6gwQ==} - - boom@0.4.2: - resolution: {integrity: sha512-OvfN8y1oAxxphzkl2SnCS+ztV/uVKTATtgLjWYg/7KwcNyf3rzpHxNQJZCKtsZd4+MteKczhWbSjtEX4bGgU9g==} - engines: {node: '>=0.8.0'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). - - bower-config@1.4.3: - resolution: {integrity: sha512-MVyyUk3d1S7d2cl6YISViwJBc2VXCkxF5AUFykvN0PQj5FsUiMNSgAYTso18oRFfyZ6XEtjrgg9MAaufHbOwNw==} - engines: {node: '>=0.8.0'} - - bower-endpoint-parser@0.2.2: - resolution: {integrity: sha512-YWZHhWkPdXtIfH3VRu3QIV95sa75O9vrQWBOHjexWCLBCTy5qJvRr36LXTqFwTchSXVlzy5piYJOjzHr7qhsNg==} - engines: {node: '>=0.8.0'} - - boxen@5.1.2: - resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} - engines: {node: '>=10'} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@2.3.2: - resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} - engines: {node: '>=0.10.0'} - - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - - broccoli-amd-funnel@2.0.1: - resolution: {integrity: sha512-VRE+0PYAN4jQfkIq3GKRj4U/4UV9rVpLan5ll6fVYV4ziVg4OEfR5GUnILEg++QtR4xSaugRxCPU5XJLDy3bNQ==} - engines: {node: '>=6'} - - broccoli-asset-rev@3.0.0: - resolution: {integrity: sha512-gAHQZnwvtl74tGevUqGuWoyOdJUdMMv0TjGSMzbdyGImr9fZcnM6xmggDA8bUawrMto9NFi00ZtNUgA4dQiUBw==} - - broccoli-asset-rewrite@2.0.0: - resolution: {integrity: sha512-dqhxdQpooNi7LHe8J9Jdxp6o3YPFWl4vQmint6zrsn2sVbOo+wpyiX3erUSt0IBtjNkAxqJjuvS375o2cLBHTA==} - - broccoli-babel-transpiler@7.8.1: - resolution: {integrity: sha512-6IXBgfRt7HZ61g67ssBc6lBb3Smw3DPZ9dEYirgtvXWpRZ2A9M22nxy6opEwJDgDJzlu/bB7ToppW33OFkA1gA==} - engines: {node: '>= 6'} - - broccoli-builder@0.18.14: - resolution: {integrity: sha512-YoUHeKnPi4xIGZ2XDVN9oHNA9k3xF5f5vlA+1wvrxIIDXqQU97gp2FxVAF503Zxdtt0C5CRB5n+47k2hlkaBzA==} - engines: {node: '>= 0.10.0'} - - broccoli-caching-writer@2.3.1: - resolution: {integrity: sha512-lfoDx98VaU8tG4mUXCxKdKyw2Lr+iSIGUjCgV83KC2zRC07SzYTGuSsMqpXFiOQlOGuoJxG3NRoyniBa1BWOqA==} - - broccoli-caching-writer@3.0.3: - resolution: {integrity: sha512-g644Kb5uBPsy+6e2DvO3sOc+/cXZQQNgQt64QQzjA9TSdP0dl5qvetpoNIx4sy/XIjrPYG1smEidq9Z9r61INw==} - - broccoli-clean-css@1.1.0: - resolution: {integrity: sha512-S7/RWWX+lL42aGc5+fXVLnwDdMtS0QEWUFalDp03gJ9Na7zj1rWa351N2HZ687E2crM9g+eDWXKzD17cbcTepg==} - - broccoli-concat@4.2.5: - resolution: {integrity: sha512-dFB5ATPwOyV8S2I7a07HxCoutoq23oY//LhM6Mou86cWUTB174rND5aQLR7Fu8FjFFLxoTbkk7y0VPITJ1IQrw==} - engines: {node: 10.* || >= 12.*} - - broccoli-config-loader@1.0.1: - resolution: {integrity: sha512-MDKYQ50rxhn+g17DYdfzfEM9DjTuSGu42Db37A8TQHQe8geYEcUZ4SQqZRgzdAI3aRQNlA1yBHJfOeGmOjhLIg==} - - broccoli-config-replace@1.1.2: - resolution: {integrity: sha512-qLlEY3V7p3ZWJNRPdPgwIM77iau1qR03S9BupMMFngjzBr7S6RSzcg96HbCYXmW9gfTbjRm9FC4CQT81SBusZg==} - - broccoli-debug@0.6.5: - resolution: {integrity: sha512-RIVjHvNar9EMCLDW/FggxFRXqpjhncM/3qq87bn/y+/zR9tqEkHvTqbyOc4QnB97NO2m6342w4wGkemkaeOuWg==} - - broccoli-file-creator@2.1.1: - resolution: {integrity: sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw==} - engines: {node: ^4.5 || 6.* || >= 7.*} - - broccoli-filter@1.3.0: - resolution: {integrity: sha512-VXJXw7eBfG82CFxaBDjYmyN7V72D4In2zwLVQJd/h3mBfF3CMdRTsv2L20lmRTtCv1sAHcB+LgMso90e/KYiLw==} - - broccoli-funnel-reducer@1.0.0: - resolution: {integrity: sha512-SaOCEdh+wnt2jFUV2Qb32m7LXyElvFwW3NKNaEJyi5PGQNwxfqpkc0KI6AbQANKgdj/40U2UC0WuGThFwuEUaA==} - - broccoli-funnel@3.0.8: - resolution: {integrity: sha512-ng4eIhPYiXqMw6SyGoxPHR3YAwEd2lr9FgBI1CyTbspl4txZovOsmzFkMkGAlu88xyvYXJqHiM2crfLa65T1BQ==} - engines: {node: 10.* || >= 12.*} - - broccoli-kitchen-sink-helpers@0.2.9: - resolution: {integrity: sha512-C+oEqivDofZv/h80rgN4WJkbZkbfwkrIeu8vFn4bb4m4jPd3ICNNplhkXGl3ps439pzc2yjZ1qIwz0yy8uHcQg==} - - broccoli-kitchen-sink-helpers@0.3.1: - resolution: {integrity: sha512-gqYnKSJxBSjj/uJqeuRAzYVbmjWhG0mOZ8jrp6+fnUIOgLN6MvI7XxBECDHkYMIFPJ8Smf4xaI066Q2FqQDnXg==} - - broccoli-merge-trees@4.2.0: - resolution: {integrity: sha512-nTrQe5AQtCrW4enLRvbD/vTLHqyW2tz+vsLXQe4IEaUhepuMGVKJJr+I8n34Vu6fPjmPLwTjzNC8izMIDMtHPw==} - engines: {node: 10.* || >= 12.*} - - broccoli-middleware@2.1.1: - resolution: {integrity: sha512-BK8aPhQpOLsHWiftrqXQr84XsvzUqeaN4PlCQOYg5yM0M+WKAHtX2WFXmicSQZOVgKDyh5aeoNTFkHjBAEBzwQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - broccoli-node-api@1.7.0: - resolution: {integrity: sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==} - - broccoli-node-info@1.1.0: - resolution: {integrity: sha512-DUohSZCdfXli/3iN6SmxPbck1OVG8xCkrLx47R25his06xVc1ZmmrOsrThiM8BsCWirwyocODiYJqNP5W2Hg1A==} - engines: {node: '>= 0.10.0'} - - broccoli-node-info@2.2.0: - resolution: {integrity: sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==} - engines: {node: 8.* || >= 10.*} - - broccoli-output-wrapper@3.2.5: - resolution: {integrity: sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==} - engines: {node: 10.* || >= 12.*} - - broccoli-persistent-filter@1.4.6: - resolution: {integrity: sha512-0RejLwoC95kv4kta8KAa+FmECJCK78Qgm8SRDEK7YyU0N9Cx6KpY3UCDy9WELl3mCXLN8TokNxc7/hp3lL4lfw==} - - broccoli-persistent-filter@2.3.1: - resolution: {integrity: sha512-hVsmIgCDrl2NFM+3Gs4Cr2TA6UPaIZip99hN8mtkaUPgM8UeVnCbxelCvBjUBHo0oaaqP5jzqqnRVvb568Yu5g==} - engines: {node: 6.* || >= 8.*} - - broccoli-persistent-filter@3.1.3: - resolution: {integrity: sha512-Q+8iezprZzL9voaBsDY3rQVl7c7H5h+bvv8SpzCZXPZgfBFCbx7KFQ2c3rZR6lW5k4Kwoqt7jG+rZMUg67Gwxw==} - engines: {node: 10.* || >= 12.*} - - broccoli-plugin@1.1.0: - resolution: {integrity: sha512-dY1QsA20of9wWEto8yhN7JQjpfjySmgeIMsvnQ9aBAv1wEJJCe04B0ekdgq7Bduyx9yWXdoC5CngGy81swmp2w==} - - broccoli-plugin@1.3.1: - resolution: {integrity: sha512-DW8XASZkmorp+q7J4EeDEZz+LoyKLAd2XZULXyD9l4m9/hAKV3vjHmB1kiUshcWAYMgTP1m2i4NnqCE/23h6AQ==} - - broccoli-plugin@2.1.0: - resolution: {integrity: sha512-ElE4caljW4slapyEhSD9jU9Uayc8SoSABWdmY9SqbV8DHNxU6xg1jJsPcMm+cXOvggR3+G+OXAYQeFjWVnznaw==} - engines: {node: 6.* || 8.* || >= 10.*} - - broccoli-plugin@4.0.7: - resolution: {integrity: sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==} - engines: {node: 10.* || >= 12.*} - - broccoli-rollup@5.0.0: - resolution: {integrity: sha512-QdMuXHwsdz/LOS8zu4HP91Sfi4ofimrOXoYP/lrPdRh7lJYD87Lfq4WzzUhGHsxMfzANIEvl/7qVHKD3cFJ4tA==} - engines: {node: '>=12.0'} - - broccoli-slow-trees@3.1.0: - resolution: {integrity: sha512-FRI7mRTk2wjIDrdNJd6znS7Kmmne4VkAkl8Ix1R/VoePFMD0g0tEl671xswzFqaRjpT9Qu+CC4hdXDLDJBuzMw==} - - broccoli-source@1.1.0: - resolution: {integrity: sha512-ahvqmwF6Yvh6l+sTJJdey4o4ynwSH8swSSBSGmUXGSPPCqBWvquWB/4rWN65ZArKilBFq/29O0yQnZNIf//sTg==} - - broccoli-source@2.1.2: - resolution: {integrity: sha512-1lLayO4wfS0c0Sj50VfHJXNWf94FYY0WUhxj0R77thbs6uWI7USiOWFqQV5dRmhAJnoKaGN4WyLGQbgjgiYFwQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - broccoli-source@3.0.1: - resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} - engines: {node: 8.* || 10.* || >= 12.*} - - broccoli-sri-hash@2.1.2: - resolution: {integrity: sha512-toLD/v7ut2ajcH8JsdCMG2Bpq2qkwTcKM6CMzVMSAJjaz/KpK69fR+gSqe1dsjh+QTdxG0yVvkq3Sij/XMzV6A==} - - broccoli-stew@1.6.0: - resolution: {integrity: sha512-sUwCJNnYH4Na690By5xcEMAZqKgquUQnMAEuIiL3Z2k63mSw9Xg+7Ew4wCrFrMmXMcLpWjZDOm6Yqnq268N+ZQ==} - engines: {node: ^4.5 || 6.* || >= 7.*} - - broccoli-stew@3.0.0: - resolution: {integrity: sha512-NXfi+Vas24n3Ivo21GvENTI55qxKu7OwKRnCLWXld8MiLiQKQlWIq28eoARaFj0lTUFwUa4jKZeA7fW9PiWQeg==} - engines: {node: 8.* || >= 10.*} - - broccoli-string-replace@0.1.2: - resolution: {integrity: sha512-QHESTrrrPlKuXQNWsvXawSQbV2g34wCZ5oKgd6bntdOuN8VHxbg1BCBHqVY5HxXJhWelimgGxj3vI7ECkyij8g==} - - broccoli-terser-sourcemap@4.1.0: - resolution: {integrity: sha512-zkNnjsAbP+M5rG2aMM1EE4BmXPUSxFKmtLUkUs2D1DLTOJQoF1xlOjGWjjKYCFy5tw8t4+tgGJ+HVa2ucJZ8sw==} - engines: {node: ^10.12.0 || 12.* || >= 14} - - broccoli-test-helper@2.0.0: - resolution: {integrity: sha512-TKwh8dBT+RcxKEG+vAoaRRhZsCMwZIHPZbCzBNCA0nUi1aoFB/LVosqwMC6H9Ipe06FxY5hpQxDLFbnBMdUPsA==} - engines: {node: 6.* || 8.* || >= 10.*} - - broccoli-uglify-sourcemap@4.0.0: - resolution: {integrity: sha512-46yB4gw1Q3ALtBROY5QfKXNXxYK5uPSvER1OGjjh2t3piaipqBfuRXTzQZvmZ+Odr6/McY+J8XmxON4+lE1ukg==} - engines: {node: ^10.12.0 || 12.* || >= 14} - - broccoli@2.3.0: - resolution: {integrity: sha512-TeYMYlCGFK8EGk4Wce1G1uU3i52+YxRqP3WPOVDojC1zUk+Gi40wHBzUT2fncQZDl26dmCQMNugtHKjvUpcGQg==} - engines: {node: '>= 6'} - - broccoli@3.5.2: - resolution: {integrity: sha512-sWi3b3fTUSVPDsz5KsQ5eCQNVAtLgkIE/HYFkEZXR/07clqmd4E/gFiuwSaqa9b+QTXc1Uemfb7TVWbEIURWDg==} - engines: {node: 8.* || >= 10.*} - - browser-process-hrtime@1.0.0: - resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} - - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - - browserslist@3.2.8: - resolution: {integrity: sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==} - hasBin: true - - browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - - bytes@1.0.0: - resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} - - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cacache@15.3.0: - resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} - engines: {node: '>= 10'} - - cache-base@1.0.1: - resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} - engines: {node: '>=0.10.0'} - - cacheable-request@6.1.0: - resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} - engines: {node: '>=8'} - - calculate-cache-key-for-tree@2.0.0: - resolution: {integrity: sha512-Quw8a6y8CPmRd6eU+mwypktYCwUcf8yVFIRbNZ6tPQEckX9yd+EBVEPC/GSZZrMWH9e7Vz4pT7XhpmyApRByLQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - can-symlink@1.0.0: - resolution: {integrity: sha512-RbsNrFyhwkx+6psk/0fK/Q9orOUr9VMxohGd8vTa4djf4TGLfblBgUfqZChrZuW0Q+mz2eBPFLusw9Jfukzmhg==} - hasBin: true - - can-write-to-dir@1.1.1: - resolution: {integrity: sha512-eOgiEWqjppB+3DN/5E82EQ8dTINus8d9GXMCbEsUnp2hcUIcXmBvzWmD3tXMk3CuBK0v+ddK9qw0EAF+JVRMjQ==} - engines: {node: '>=10.13'} - - caniuse-lite@1.0.30001470: - resolution: {integrity: sha512-065uNwY6QtHCBOExzbV6m236DDhYCCtPmQUCoQtwkVqzud8v5QPidoMr6CoMkC2nfp6nksjttqWQRRh75LqUmA==} - - caniuse-lite@1.0.30001614: - resolution: {integrity: sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==} - - capture-exit@2.0.0: - resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} - engines: {node: 6.* || 8.* || >= 10.*} - - cardinal@1.0.0: - resolution: {integrity: sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg==} - hasBin: true - - chai-as-promised@6.0.0: - resolution: {integrity: sha512-Zf5Dq6p4d0pApi662BtRe95oKYbEyNb+TLbIdwVSlewYxVhtMYwrTD3TAmcaf1XanuBw7egusnLxLXlMnv0myw==} - peerDependencies: - chai: '>= 2.1.2 < 4' - - chai-as-promised@7.1.1: - resolution: {integrity: sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==} - peerDependencies: - chai: '>= 2.1.2 < 5' - - chai-files@1.4.0: - resolution: {integrity: sha512-tPTx7H2kpR+wILWHRx8RxpXcRUdc2uH8su505C9R3p5GA+eYbZBXuxWC0RZbyElYi7X7Fp/V/S2PQjkakrT1mQ==} - - chai@3.5.0: - resolution: {integrity: sha512-eRYY0vPS2a9zt5w5Z0aCeWbrXTEyvk7u/Xf71EzNObrjSCPgMm1Nku/D/u2tiqHBX5j40wWhj54YJLtgn8g55A==} - engines: {node: '>= 0.4.0'} - - chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} - engines: {node: '>=4'} - - chalk@1.1.3: - resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} - engines: {node: '>=0.10.0'} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - - charm@1.0.2: - resolution: {integrity: sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw==} - - check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} - - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - - chrome-trace-event@1.0.3: - resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} - engines: {node: '>=6.0'} - - ci-info@3.8.0: - resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} - engines: {node: '>=8'} - - class-utils@0.3.6: - resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} - engines: {node: '>=0.10.0'} - - clean-base-url@1.0.0: - resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} - - clean-css-promise@0.1.1: - resolution: {integrity: sha512-tzWkANXMD70ETa/wAu2TXAAxYWS0ZjVUFM2dVik8RQBoAbGMFJv4iVluz3RpcoEbo++fX4RV/BXfgGoOjp8o3Q==} - - clean-css@3.4.28: - resolution: {integrity: sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==} - engines: {node: '>=0.10.0'} - hasBin: true - - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - - clean-up-path@1.0.0: - resolution: {integrity: sha512-PHGlEF0Z6976qQyN6gM7kKH6EH0RdfZcc8V+QhFe36eRxV0SMH5OUBZG7Bxa9YcreNzyNbK63cGiZxdSZgosRw==} - - cli-boxes@2.2.1: - resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} - engines: {node: '>=6'} - - cli-columns@4.0.0: - resolution: {integrity: sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==} - engines: {node: '>= 10'} - - cli-cursor@2.1.0: - resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} - engines: {node: '>=4'} - - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - - cli-spinners@2.7.0: - resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} - engines: {node: '>=6'} - - cli-table3@0.6.3: - resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} - engines: {node: 10.* || >= 12.*} - - cli-table@0.3.11: - resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} - engines: {node: '>= 0.2.0'} - - cli-width@2.2.1: - resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} - - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collection-visit@1.0.0: - resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} - engines: {node: '>=0.10.0'} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - - colors@1.0.3: - resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} - engines: {node: '>=0.1.90'} - - combined-stream@0.0.7: - resolution: {integrity: sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==} - engines: {node: '>= 0.8'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@2.8.1: - resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} - engines: {node: '>= 0.6.x'} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - component-emitter@1.3.0: - resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} - - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - - configstore@5.0.1: - resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} - engines: {node: '>=8'} - - connect@3.7.0: - resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} - engines: {node: '>= 0.10.0'} - - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - - console-ui@3.1.2: - resolution: {integrity: sha512-+5j3R4wZJcEYZeXk30whc4ZU/+fWW9JMTNntVuMYpjZJ9n26Cxr0tUBXco1NRjVZRpRVvZ4DDKKKIHNYeUG9Dw==} - engines: {node: 6.* || 8.* || >= 10.*} - - consolidate@0.16.0: - resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==} - engines: {node: '>= 0.10.0'} - peerDependencies: - arc-templates: ^0.5.3 - atpl: '>=0.7.6' - babel-core: ^6.26.3 - bracket-template: ^1.1.5 - coffee-script: ^1.12.7 - dot: ^1.1.3 - dust: ^0.3.0 - dustjs-helpers: ^1.7.4 - dustjs-linkedin: ^2.7.5 - eco: ^1.1.0-rc-3 - ect: ^0.5.9 - ejs: ^3.1.5 - haml-coffee: ^1.14.1 - hamlet: ^0.3.3 - hamljs: ^0.6.2 - handlebars: ^4.7.6 - hogan.js: ^3.0.2 - htmling: ^0.0.8 - jade: ^1.11.0 - jazz: ^0.0.18 - jqtpl: ~1.1.0 - just: ^0.1.8 - liquid-node: ^3.0.1 - liquor: ^0.0.5 - lodash: ^4.17.20 - marko: ^3.14.4 - mote: ^0.2.0 - mustache: ^4.0.1 - nunjucks: ^3.2.2 - plates: ~0.4.11 - pug: ^3.0.0 - qejs: ^3.0.5 - ractive: ^1.3.12 - razor-tmpl: ^1.3.1 - react: ^16.13.1 - react-dom: ^16.13.1 - slm: ^2.0.0 - squirrelly: ^5.1.0 - swig: ^1.4.2 - swig-templates: ^2.0.3 - teacup: ^2.0.0 - templayed: '>=0.2.3' - then-jade: '*' - then-pug: '*' - tinyliquid: ^0.2.34 - toffee: ^0.3.6 - twig: ^1.15.2 - twing: ^5.0.2 - underscore: ^1.11.0 - vash: ^0.13.0 - velocityjs: ^2.0.1 - walrus: ^0.10.1 - whiskers: ^0.4.0 - peerDependenciesMeta: - arc-templates: - optional: true - atpl: - optional: true - babel-core: - optional: true - bracket-template: - optional: true - coffee-script: - optional: true - dot: - optional: true - dust: - optional: true - dustjs-helpers: - optional: true - dustjs-linkedin: - optional: true - eco: - optional: true - ect: - optional: true - ejs: - optional: true - haml-coffee: - optional: true - hamlet: - optional: true - hamljs: - optional: true - handlebars: - optional: true - hogan.js: - optional: true - htmling: - optional: true - jade: - optional: true - jazz: - optional: true - jqtpl: - optional: true - just: - optional: true - liquid-node: - optional: true - liquor: - optional: true - lodash: - optional: true - marko: - optional: true - mote: - optional: true - mustache: - optional: true - nunjucks: - optional: true - plates: - optional: true - pug: - optional: true - qejs: - optional: true - ractive: - optional: true - razor-tmpl: - optional: true - react: - optional: true - react-dom: - optional: true - slm: - optional: true - squirrelly: - optional: true - swig: - optional: true - swig-templates: - optional: true - teacup: - optional: true - templayed: - optional: true - then-jade: - optional: true - then-pug: - optional: true - tinyliquid: - optional: true - toffee: - optional: true - twig: - optional: true - twing: - optional: true - underscore: - optional: true - vash: - optional: true - velocityjs: - optional: true - walrus: - optional: true - whiskers: - optional: true - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-tag@2.0.1: - resolution: {integrity: sha512-jxsETSDs5NbNwyiDuIp672fUMhUyu8Qxc5MOBOJOcgW/fQESI6o5K1LBDrnEE7Bh810a685lWEZHTF4jQYGEEw==} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - continuable-cache@0.3.1: - resolution: {integrity: sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==} - - convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - - cookie@0.4.2: - resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} - engines: {node: '>= 0.6'} - - cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - - copy-dereference@1.0.0: - resolution: {integrity: sha512-40TSLuhhbiKeszZhK9LfNdazC67Ue4kq/gGwN5sdxEUWPXTIMmKmGmgD9mPfNKVAeecEW+NfEIpBaZoACCQLLw==} - - copy-descriptor@0.1.1: - resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} - engines: {node: '>=0.10.0'} - - core-js-compat@3.29.1: - resolution: {integrity: sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==} - - core-js-compat@3.37.0: - resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==} - - core-js@2.6.12: - resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. - - core-object@3.1.5: - resolution: {integrity: sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==} - engines: {node: '>= 4'} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - - cross-spawn@6.0.5: - resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} - engines: {node: '>=4.8'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - cryptiles@0.2.2: - resolution: {integrity: sha512-gvWSbgqP+569DdslUiCelxIv3IYK5Lgmq1UrRnk+s1WxQOQ16j3GPDcjdtgL5Au65DU/xQi6q3xPtf5Kta+3IQ==} - engines: {node: '>=0.8.0'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). - - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - - css-loader@5.2.7: - resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.27.0 || ^5.0.0 - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} - - cssom@0.3.8: - resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - - cssom@0.4.4: - resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} - - cssom@0.5.0: - resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - - cssstyle@2.3.0: - resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} - engines: {node: '>=8'} - - ctype@0.5.3: - resolution: {integrity: sha512-T6CEkoSV4q50zW3TlTHMbzy1E5+zlnNcY+yb7tWVYlTwPhx9LpnfAkd4wecpWknDyptp4k97LUZeInlf6jdzBg==} - engines: {node: '>= 0.4'} - - dag-map@2.0.2: - resolution: {integrity: sha512-xnsprIzYuDeiyu5zSKwilV/ajRHxnoMlAhEREfyfTgTSViMVY2fGP1ZcHJbtwup26oCkofySU/m6oKJ3HrkW7w==} - - data-uri-to-buffer@2.0.2: - resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} - - data-urls@2.0.0: - resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} - engines: {node: '>=10'} - - data-urls@3.0.2: - resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} - engines: {node: '>=12'} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - - decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - - decompress-response@3.3.0: - resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} - engines: {node: '>=4'} - - deep-eql@0.1.3: - resolution: {integrity: sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==} - - deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - defer-to-connect@1.1.3: - resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} - - define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} - - define-property@0.2.5: - resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} - engines: {node: '>=0.10.0'} - - define-property@1.0.0: - resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} - engines: {node: '>=0.10.0'} - - define-property@2.0.2: - resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} - engines: {node: '>=0.10.0'} - - del@5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - - delayed-stream@0.0.5: - resolution: {integrity: sha512-v+7uBd1pqe5YtgPacIIbZ8HuHeLFVNe4mUEyFDXL6KiqzEykjbw+5mXZXpGFgNVasdL4jWKgaKIXrEHiynN1LA==} - engines: {node: '>=0.4.0'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - - depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - detect-file@1.0.0: - resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} - engines: {node: '>=0.10.0'} - - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - dettle@1.0.2: - resolution: {integrity: sha512-pIVcNUx2/R7P45l3wEUsyrZcfbVUCKBmctUN41syh2asCXmrRndJEiNng9+8socNOAEBiBRqsQCh3HhCkOFwwg==} - - diff@5.0.0: - resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} - engines: {node: '>=0.3.1'} - - diff@5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - domexception@2.0.1: - resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} - engines: {node: '>=8'} - - domexception@4.0.0: - resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} - engines: {node: '>=12'} - - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - - duplexer3@0.1.5: - resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - - editions@1.3.4: - resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} - engines: {node: '>=0.8'} - - editions@2.3.1: - resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} - engines: {node: '>=0.8'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - electron-to-chromium@1.4.340: - resolution: {integrity: sha512-zx8hqumOqltKsv/MF50yvdAlPF9S/4PXbyfzJS6ZGhbddGkRegdwImmfSVqCkEziYzrIGZ/TlrzBND4FysfkDg==} - - electron-to-chromium@1.4.752: - resolution: {integrity: sha512-P3QJreYI/AUTcfBVrC4zy9KvnZWekViThgQMX/VpJ+IsOBbcX5JFpORM4qWapwWQ+agb2nYAOyn/4PMXOk0m2Q==} - - ember-auto-import@2.6.1: - resolution: {integrity: sha512-3bCRi/pXp4QslmuCXGlSz9xwR7DF5oDx3zZO5OXKzNZihtkqAM1xvGuRIdQSl46pvbAXOkp8Odl5fOen1i0dRw==} - engines: {node: 12.* || 14.* || >= 16} - - ember-cache-primitive-polyfill@1.0.1: - resolution: {integrity: sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw==} - engines: {node: 10.* || >= 12} - - ember-cached-decorator-polyfill@1.0.1: - resolution: {integrity: sha512-VDgrpIJ6rDDHIfkYrsFR1BM3fpcC0+zFWIOsX0qY44zPrIXjhQWVXs2iVXLIPHprSgf+tFQ3ESxwDscpeRe/0A==} - engines: {node: 14.* || >= 16} - peerDependencies: - ember-source: ^3.13.0 || ^4.0.0 - - ember-cli-app-version@6.0.0: - resolution: {integrity: sha512-XhzETSTy+RMTIyxM/FaZ/8aJvAwT/iIp8HC9zukpOaSPEm5i6Vm4tskeXY4OBnY3VwFWNXWssDt1hgIkUP76WQ==} - engines: {node: 14.* || 16.* || >= 18} - peerDependencies: - ember-source: ^3.28.0 || ^4.0.0 - - ember-cli-babel-plugin-helpers@1.1.1: - resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-babel@7.26.11: - resolution: {integrity: sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-blueprint-test-helpers@0.19.2: - resolution: {integrity: sha512-otCKdGcNFK0+MkQo+LLjYbRD9EerApH6Z/odvvlL1hxrN+owHMV5E+jI2rbtdvNEH0/6w5ZqjH4kS232fvtCxQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-dependency-checker@3.3.1: - resolution: {integrity: sha512-Tg6OeijjXNKWkDm6057Tr0N9j9Vlz/ITewXWpn1A/+Wbt3EowBx5ZKfvoupqz05EznKgL1B/ecG0t+JN7Qm6MA==} - engines: {node: '>= 6'} - peerDependencies: - ember-cli: ^3.2.0 || ^4.0.0 - - ember-cli-fastboot-testing@0.6.0: - resolution: {integrity: sha512-ppZyrb4qKjaF+JqsHgDtSic04V5Ut4i1z3FkT2n0IizzL1duQN3rqQgE0sKa8wR7B4tBd7Fc+JQL3VIkVwzBow==} - engines: {node: 12.* || 14.* || >= 16} - - ember-cli-fastboot@4.1.0: - resolution: {integrity: sha512-OQ1+eEVK8OBh0M0KY6Rrxu2JqdAByQlY4SWzR/06cTftcDSKpUL44DTrWPs7Te4bEgQfjbic2g8AUOVw+2YvUA==} - engines: {node: 14.* || 16.* || >=18} - - ember-cli-get-component-path-option@1.0.0: - resolution: {integrity: sha512-k47TDwcJ2zPideBCZE8sCiShSxQSpebY2BHcX2DdipMmBox5gsfyVrbKJWIHeSTTKyEUgmBIvQkqTOozEziCZA==} - - ember-cli-htmlbars@6.2.0: - resolution: {integrity: sha512-j5EGixjGau23HrqRiW/JjoAovg5UBHfjbyN7wX5ekE90knIEqUUj1z/Mo/cTx/J2VepQ2lE6HdXW9LWQ/WdMtw==} - engines: {node: 12.* || 14.* || >= 16} - - ember-cli-inject-live-reload@2.1.0: - resolution: {integrity: sha512-YV5wYRD5PJHmxaxaJt18u6LE6Y+wo455BnmcpN+hGNlChy2piM9/GMvYgTAz/8Vin8RJ5KekqP/w/NEaRndc/A==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-internal-test-helpers@0.9.1: - resolution: {integrity: sha512-ia+p7LrAe2tENG+Vewdi93kGlsI7OkjB7tEakTtCELkIvZpmPX+uYGhIi5nVOynLiej2M81MQmNqB8jX93ejqQ==} - - ember-cli-is-package-missing@1.0.0: - resolution: {integrity: sha512-9hEoZj6Au5onlSDdcoBqYEPT8ehlYntZPxH8pBKV0GO7LNel88otSAQsCfXvbi2eKE+MaSeLG/gNaCI5UdWm9g==} - - ember-cli-lodash-subset@2.0.1: - resolution: {integrity: sha512-QkLGcYv1WRK35g4MWu/uIeJ5Suk2eJXKtZ+8s+qE7C9INmpCPyPxzaqZABquYzcWNzIdw6kYwz3NWAFdKYFxwg==} - engines: {node: ^4.5 || 6.* || >= 7.*} - - ember-cli-normalize-entity-name@1.0.0: - resolution: {integrity: sha512-rF4P1rW2P1gVX1ynZYPmuIf7TnAFDiJmIUFI1Xz16VYykUAyiOCme0Y22LeZq8rTzwBMiwBwoE3RO4GYWehXZA==} - - ember-cli-path-utils@1.0.0: - resolution: {integrity: sha512-Qq0vvquzf4cFHoDZavzkOy3Izc893r/5spspWgyzLCPTaG78fM3HsrjZm7UWEltbXUqwHHYrqZd/R0jS08NqSA==} - - ember-cli-preprocess-registry@3.3.0: - resolution: {integrity: sha512-60GYpw7VPeB7TvzTLZTuLTlHdOXvayxjAQ+IxM2T04Xkfyu75O2ItbWlftQW7NZVGkaCsXSRAmn22PG03VpLMA==} - - ember-cli-sri@2.1.1: - resolution: {integrity: sha512-YG/lojDxkur9Bnskt7xB6gUOtJ6aPl/+JyGYm9HNDk3GECVHB3SMN3rlGhDKHa1ndS5NK2W2TSLb9bzRbGlMdg==} - engines: {node: '>= 0.10.0'} - - ember-cli-string-utils@1.1.0: - resolution: {integrity: sha512-PlJt4fUDyBrC/0X+4cOpaGCiMawaaB//qD85AXmDRikxhxVzfVdpuoec02HSiTGTTB85qCIzWBIh8lDOiMyyFg==} - - ember-cli-terser@4.0.2: - resolution: {integrity: sha512-Ej77K+YhCZImotoi/CU2cfsoZaswoPlGaM5TB3LvjvPDlVPRhxUHO2RsaUVC5lsGeRLRiHCOxVtoJ6GyqexzFA==} - engines: {node: 10.* || 12.* || >= 14} - - ember-cli-test-info@1.0.0: - resolution: {integrity: sha512-dEVTIpmUfCzweC97NGf6p7L6XKBwV2GmSM4elmzKvkttEp5P7AvGA9uGyN4GqFq+RwhW+2b0I2qlX00w+skm+A==} - - ember-cli-test-loader@3.0.0: - resolution: {integrity: sha512-wfFRBrfO9gaKScYcdQxTfklx9yp1lWK6zv1rZRpkas9z2SHyJojF7NOQRWQgSB3ypm7vfpiF8VsFFVVr7VBzAQ==} - engines: {node: 10.* || >= 12} - - ember-cli-typescript-blueprint-polyfill@0.1.0: - resolution: {integrity: sha512-g0weUTOnHmPGqVZzkQTl3Nbk9fzEdFkEXydCs5mT1qBjXh8eQ6VlmjjGD5/998UXKuA0pLSCVVMbSp/linLzGA==} - - ember-cli-typescript@2.0.2: - resolution: {integrity: sha512-7I5azCTxOgRDN8aSSnJZIKSqr+MGnT+jLTUbBYqF8wu6ojs2DUnTePxUcQMcvNh3Q3B1ySv7Q/uZFSjdU9gSjA==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-typescript@3.0.0: - resolution: {integrity: sha512-lo5YArbJzJi5ssvaGqTt6+FnhTALnSvYVuxM7lfyL1UCMudyNJ94ovH5C7n5il7ATd6WsNiAPRUO/v+s5Jq/aA==} - engines: {node: 8.* || >= 10.*} - - ember-cli-version-checker@3.1.3: - resolution: {integrity: sha512-PZNSvpzwWgv68hcXxyjREpj3WWb81A7rtYNQq1lLEgrWIchF8ApKJjWP3NBpHjaatwILkZAV8klair5WFlXAKg==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-cli-version-checker@4.1.1: - resolution: {integrity: sha512-bzEWsTMXUGEJfxcAGWPe6kI7oHEGD3jaxUWDYPTqzqGhNkgPwXTBgoWs9zG1RaSMaOPFnloWuxRcoHi4TrYS3Q==} - engines: {node: 8.* || 10.* || >= 12.*} - - ember-cli-version-checker@5.1.2: - resolution: {integrity: sha512-rk7GY+FmLn/2e22HsZs0Ycrz8HQ1W3Fv+2TFOuEFW9optnDXDgkntPBIl6gact/LHsfBM5RKbM3dHsIIeLgl0Q==} - engines: {node: 10.* || >= 12.*} - - ember-cli@4.11.0: - resolution: {integrity: sha512-X0Ep67O/r2nCViILV8wEvI0xiRlLRS8GgeDklQ3SvDXQp2d3xbI8ARW76pcb1du39HPgIi0G6F/OpJ1uOr4ZQQ==} - engines: {node: '>= 14'} - hasBin: true - - ember-compatibility-helpers@1.2.6: - resolution: {integrity: sha512-2UBUa5SAuPg8/kRVaiOfTwlXdeVweal1zdNPibwItrhR0IvPrXpaqwJDlEZnWKEoB+h33V0JIfiWleSG6hGkkA==} - engines: {node: 10.* || >= 12.*} - - ember-data@file:packages/-ember-data: - resolution: {directory: packages/-ember-data, type: directory} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember/string': ^3.0.1 || ^4.0.0 - - ember-decorators-polyfill@1.1.5: - resolution: {integrity: sha512-O154i8sLoVjsiKzSqxGRfHGr+N+drT6mRrLDbNgJCnW/V5uLg/ppZFpUsrdxuXnp5Q9us3OfXV1nX2CH+7bUpA==} - engines: {node: 8.* || >= 10.*} - - ember-destroyable-polyfill@2.0.3: - resolution: {integrity: sha512-TovtNqCumzyAiW0/OisSkkVK93xnVF4NRU6+FN0ubpfwEOpRrmM2RqDwXI6YAChCgSHON1cz0DfQStpA1Gjuuw==} - engines: {node: 10.* || >= 12} - - ember-disable-prototype-extensions@1.1.3: - resolution: {integrity: sha512-SB9NcZ27OtoUk+gfalsc3QU17+54OoqR668qHcuvHByk4KAhGxCKlkm9EBlKJcGr7yceOOAJqohTcCEBqfRw9g==} - engines: {node: '>= 0.10.0'} - - ember-export-application-global@2.0.1: - resolution: {integrity: sha512-B7wiurPgsxsSGzJuPFkpBWnaeuCu2PGpG2BjyrfA1VcL7//o+5RSnZqiCEY326y7qmxb2GoCgo0ft03KBU0rRw==} - engines: {node: '>= 4'} - - ember-get-config@2.1.1: - resolution: {integrity: sha512-uNmv1cPG/4qsac8oIf5txJ2FZ8p88LEpG4P3dNcjsJS98Y8hd0GPMFwVqpnzI78Lz7VYRGQWY4jnE4qm5R3j4g==} - engines: {node: 12.* || 14.* || >= 16} - - ember-inflector@4.0.2: - resolution: {integrity: sha512-+oRstEa52mm0jAFzhr51/xtEWpCEykB3SEBr7vUg8YnXUZJ5hKNBppP938q8Zzr9XfJEbzrtDSGjhKwJCJv6FQ==} - engines: {node: 10.* || 12.* || >= 14} - - ember-load-initializers@2.1.2: - resolution: {integrity: sha512-CYR+U/wRxLbrfYN3dh+0Tb6mFaxJKfdyz+wNql6cqTrA0BBi9k6J3AaKXj273TqvEpyyXegQFFkZEiuZdYtgJw==} - engines: {node: 6.* || 8.* || >= 10.*} - - ember-maybe-import-regenerator@1.0.0: - resolution: {integrity: sha512-wtjgjEV0Hk4fgiAwFjOfPrGWfmFrbRW3zgNZO4oA3H5FlbMssMvWuR8blQ3QSWYHODVK9r+ThsRAs8lG4kbxqA==} - engines: {node: '>= 12.*'} - - ember-qunit@6.2.0: - resolution: {integrity: sha512-mC+0bp8DwWzJLn8SW3GS8KDZIkl4yLsNYwMi5Dw6+aFllq7FM2crd/dfY4MuOIHK7GKdjtmWJTMGnjSpeSayaw==} - engines: {node: 14.* || 16.* || >= 18} - peerDependencies: - '@ember/test-helpers': ^2.9.3 - ember-source: '>=3.28' - qunit: ^2.13.0 - - ember-resolver@10.0.0: - resolution: {integrity: sha512-e99wFJ4ZpleJ6JMEcIk4WEYP4s3nc+9/iNSXtwBHXC8ADJHJTeN3HjnT/eEbFbswdui4FYxIYuK+UCdP09811Q==} - engines: {node: 14.* || 16.* || >= 18} - peerDependencies: - '@ember/string': ^3.0.1 - ember-source: ^4.8.3 - peerDependenciesMeta: - ember-source: - optional: true - - ember-rfc176-data@0.3.18: - resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} - - ember-router-generator@2.0.0: - resolution: {integrity: sha512-89oVHVJwmLDvGvAUWgS87KpBoRhy3aZ6U0Ql6HOmU4TrPkyaa8pM0W81wj9cIwjYprcQtN9EwzZMHnq46+oUyw==} - engines: {node: 8.* || 10.* || >= 12} - - ember-simple-tree@0.8.3: - resolution: {integrity: sha512-JEQ4ccXUzThaZiyVT+b3fG76JndG7d4BCRp/Zg5nEhc+u+/ZHHRhSIWVXxPXY8K6pyFcLVSmEscd27hUNDqbww==} - engines: {node: 12.* || 14.* || >= 16} - - ember-source-channel-url@3.0.0: - resolution: {integrity: sha512-vF/8BraOc66ZxIDo3VuNP7iiDrnXEINclJgSJmqwAAEpg84Zb1DHPI22XTXSDA+E8fW5btPUxu65c3ZXi8AQFA==} - engines: {node: 10.* || 12.* || >= 14} - hasBin: true - - ember-source@4.12.0: - resolution: {integrity: sha512-h0lV902A4Mny2eiqXPy15uXXoCc7BnUegE4axLAy4IoxEkJ1o5h0aLJFiB4Tzb1htx8vgHjJz//Y5Jig7NSDTw==} - engines: {node: '>= 14.*'} - peerDependencies: - '@glimmer/component': ^1.1.2 - - ember-source@5.7.0: - resolution: {integrity: sha512-iOZVyxLBzGewEThDDsNRZ9y02SNH42PWSPC9U4O94pew7ktld3IpIODCDjLCtKWn2zAGM9DhWTMrXz27HI1UKw==} - engines: {node: '>= 16.*'} - peerDependencies: - '@glimmer/component': ^1.1.2 - - ember-strict-resolver@1.3.0: - resolution: {integrity: sha512-GeI1LLLt470sjaq/huKGQTDJPDOH0FlrX8FFVcSZPXO2U9FQH7Kc8BaXb4GpViJbfLLC4d7tIUZI4NBnuXSmKg==} - engines: {node: 10.* || >= 12} - - ember-template-imports@3.4.2: - resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} - engines: {node: 12.* || >= 14} - - ember-try-config@4.0.0: - resolution: {integrity: sha512-jAv7fqYJK7QYYekPc/8Nr7KOqDpv/asqM6F8xcRnbmf9UrD35BkSffY63qUuiD9e0aR5qiMNBIQzH8f65rGDqw==} - engines: {node: 10.* || 12.* || >= 14} - - ember-try@2.0.0: - resolution: {integrity: sha512-2N7Vic45sbAegA5fhdfDjVbEVS4r+ze+tTQs2/wkY+8fC5yAGHfCM5ocyoTfBN5m7EhznC3AyMsOy4hLPzHFSQ==} - engines: {node: 10.* || 12.* || >= 14.*} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - - encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - engine.io-parser@5.0.6: - resolution: {integrity: sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==} - engines: {node: '>=10.0.0'} - - engine.io@6.4.1: - resolution: {integrity: sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==} - engines: {node: '>=10.0.0'} - - enhanced-resolve@5.12.0: - resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} - engines: {node: '>=10.13.0'} - - ensure-posix-path@1.1.1: - resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} - - entities@1.1.2: - resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - - entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} - engines: {node: '>=0.12'} - - err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - - errlop@2.2.0: - resolution: {integrity: sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==} - engines: {node: '>=0.8'} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - error@7.2.1: - resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} - - es-abstract@1.21.2: - resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} - engines: {node: '>= 0.4'} - - es-module-lexer@0.9.3: - resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} - - es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} - - es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - escodegen@2.0.0: - resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} - engines: {node: '>=6.0'} - hasBin: true - - eslint-config-prettier@8.8.0: - resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} - - eslint-module-utils@2.7.4: - resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-ember@11.4.9: - resolution: {integrity: sha512-1nAgMSwAJrntfvsiyIyHtYY+AvAPZ1wxcoa+gsui/VviQqULCtZLF8+1UAOTAGJ5bEjhe3f8TTcyRGHXRPHzeQ==} - engines: {node: 14.* || 16.* || >= 18} - peerDependencies: - eslint: '>= 7' - - eslint-plugin-es@3.0.1: - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-import@2.27.5: - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-mocha@10.1.0: - resolution: {integrity: sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==} - engines: {node: '>=14.0.0'} - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-node@11.1.0: - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=5.16.0' - - eslint-plugin-prettier@4.2.1: - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - - eslint-plugin-qunit@7.3.4: - resolution: {integrity: sha512-EbDM0zJerH9zVdUswMJpcFF7wrrpvsGuYfNexUpa5hZkkdFhaFcX+yD+RSK4Nrauw4psMGlcqeWUMhaVo+Manw==} - engines: {node: 12.x || 14.x || >=16.0.0} - - eslint-plugin-simple-import-sort@10.0.0: - resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} - peerDependencies: - eslint: '>=5.0.0' - - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - - eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.4.0: - resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.37.0: - resolution: {integrity: sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - - espree@9.5.1: - resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@3.0.0: - resolution: {integrity: sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw==} - engines: {node: '>=0.10.0'} - hasBin: true - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - events-to-array@1.1.2: - resolution: {integrity: sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - exec-sh@0.3.6: - resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} - - execa@1.0.0: - resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} - engines: {node: '>=6'} - - execa@2.1.0: - resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} - engines: {node: ^8.12.0 || >=9.7.0} - - execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exists-sync@0.0.3: - resolution: {integrity: sha512-/qPB5E0cRuA/Cs5vHrmKYSfhIBCPJs9Vm3e9aIejMwwbe6idMeNbGu1g5stvr/bXT6HywHckLPEkmY7HK6FlwA==} - deprecated: Please replace with usage of fs.existsSync - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - - expand-brackets@2.1.4: - resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} - engines: {node: '>=0.10.0'} - - expand-tilde@2.0.2: - resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} - engines: {node: '>=0.10.0'} - - express@4.18.2: - resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} - engines: {node: '>= 0.10.0'} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend-shallow@3.0.2: - resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} - engines: {node: '>=0.10.0'} - - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - - extglob@2.0.4: - resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} - engines: {node: '>=0.10.0'} - - extract-stack@2.0.0: - resolution: {integrity: sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==} - engines: {node: '>=8'} - - fake-xml-http-request@2.1.2: - resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-diff@1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} - - fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - - fast-sourcemap-concat@1.4.0: - resolution: {integrity: sha512-x90Wlx/2C83lfyg7h4oguTZN4MyaVfaiUSJQNpU+YEA0Odf9u659Opo44b0LfoVg9G/bOE++GdID/dkyja+XcA==} - engines: {node: '>= 4'} - - fast-sourcemap-concat@2.1.0: - resolution: {integrity: sha512-L9uADEnnHOeF4U5Kc3gzEs3oFpNCFkiTJXvT+nKmR0zcFqHZJJbszWT7dv4t9558FJRGpCj8UxUpTgz2zwiIZA==} - engines: {node: 10.* || >= 12.*} - - fastboot-express-middleware@4.1.0: - resolution: {integrity: sha512-RH/YnAn8S/CuzVsK0SLH5cVDP50R3MedqPFhf69T3z6h7LSFm12VAU2TKkqtW/N7qDs00CK1Clus2CwuzXVtWA==} - engines: {node: 12.* || 14.* || >=16} - - fastboot-transform@0.1.3: - resolution: {integrity: sha512-6otygPIJw1ARp1jJb+6KVO56iKBjhO+5x59RSC9qiZTbZRrv+HZAuP00KD3s+nWMvcFDemtdkugki9DNFTTwCQ==} - - fastboot@3.3.2: - resolution: {integrity: sha512-2NKTW32GvEsDyBrdw1trW1JsbS+9/7sAQuKwkht12mNitimRrSKVLP2AxsM/HSXQE+aiET4XCfKdyeIy0kQbKQ==} - engines: {node: 12.* || 14.* || >=16} - - fastboot@4.1.0: - resolution: {integrity: sha512-NoUUiWhLS4JfrYiHmbRjqqWmzttO+6hVyt+CawsZCg79ljTGAIhPThy8cxg11yiDNiaM9EY3kocWZrCMUM7o4g==} - engines: {node: 12.* || 14.* || >=16} - - fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - - figures@2.0.0: - resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} - engines: {node: '>=4'} - - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - filesize@10.0.6: - resolution: {integrity: sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g==} - engines: {node: '>= 10.4.0'} - - filesize@10.1.1: - resolution: {integrity: sha512-L0cdwZrKlwZQkMSFnCflJ6J2Y+5egO/p3vgRSDQGxQt++QbUZe5gMbRO6kg6gzwQDPvq2Fk9AmoxUNfZ5gdqaQ==} - engines: {node: '>= 10.4.0'} - - filesize@5.0.3: - resolution: {integrity: sha512-RM123v6KPqgZJmVCh4rLvCo8tLKr4sgD92DeZ+AuoUE8teGZJHKs1cTORwETcpIJSlGsz2WYdwKDQUXby5hNqQ==} - engines: {node: '>= 0.4.0'} - - fill-range@4.0.0: - resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} - engines: {node: '>=0.10.0'} - - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - - finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - - finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} - - find-babel-config@1.2.0: - resolution: {integrity: sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==} - engines: {node: '>=4.0.0'} - - find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} - - find-index@1.1.1: - resolution: {integrity: sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==} - - find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} - - find-up@2.1.0: - resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} - engines: {node: '>=4'} - - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - find-yarn-workspace-root@1.2.1: - resolution: {integrity: sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==} - - find-yarn-workspace-root@2.0.0: - resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} - - findup-sync@2.0.0: - resolution: {integrity: sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==} - engines: {node: '>= 0.10'} - - findup-sync@4.0.0: - resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} - engines: {node: '>= 8'} - - fireworm@0.7.2: - resolution: {integrity: sha512-GjebTzq+NKKhfmDxjKq3RXwQcN9xRmZWhnnuC9L+/x5wBQtR0aaQM50HsjrzJ2wc28v1vSdfOpELok0TKR4ddg==} - - fixturify-project@1.10.0: - resolution: {integrity: sha512-L1k9uiBQuN0Yr8tA9Noy2VSQ0dfg0B8qMdvT7Wb5WQKc7f3dn3bzCbSrqlb+etLW+KDV4cBC7R1OvcMg3kcxmA==} - - fixturify-project@2.1.1: - resolution: {integrity: sha512-sP0gGMTr4iQ8Kdq5Ez0CVJOZOGWqzP5dv/veOTdFNywioKjkNWCHBi1q65DMpcNGUGeoOUWehyji274Q2wRgxA==} - engines: {node: 10.* || >= 12.*} - - fixturify@0.3.4: - resolution: {integrity: sha512-Gx+KSB25b6gMc4bf7UFRTA85uE0iZR+RYur0JHh6dg4AGBh0EksOv4FCHyM7XpGmiJO7Bc7oV7vxENQBT+2WEQ==} - - fixturify@1.3.0: - resolution: {integrity: sha512-tL0svlOy56pIMMUQ4bU1xRe6NZbFSa/ABTWMxW2mH38lFGc9TrNAKWcMBQ7eIjo3wqSS8f2ICabFaatFyFmrVQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - fixturify@2.1.1: - resolution: {integrity: sha512-SRgwIMXlxkb6AUgaVjIX+jCEqdhyXu9hah7mcK+lWynjKtX73Ux1TDv71B7XyaQ+LJxkYRHl5yCL8IycAvQRUw==} - engines: {node: 10.* || >= 12.*} - - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - - flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - - follow-redirects@1.15.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - - for-in@1.0.2: - resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} - engines: {node: '>=0.10.0'} - - forever-agent@0.5.2: - resolution: {integrity: sha512-PDG5Ef0Dob/JsZUxUltJOhm/Y9mlteAE+46y3M9RBz/Rd3QVENJ75aGRhN56yekTUboaBIkd8KVWX2NjF6+91A==} - - form-data@0.1.4: - resolution: {integrity: sha512-x8eE+nzFtAMA0YYlSxf/Qhq6vP1f8wSoZ7Aw1GuctBcmudCNuTUmmx45TfEplyb6cjsZO/jvh6+1VpZn24ez+w==} - engines: {node: '>= 0.8'} - - form-data@3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} - - form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fragment-cache@0.2.1: - resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} - engines: {node: '>=0.10.0'} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - - fromentries@1.3.2: - resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} - - fs-extra@0.24.0: - resolution: {integrity: sha512-w1RvhdLZdU9V3vQdL+RooGlo6b9R9WVoBanOfoJvosWlqSKvrjFlci2oVhwvLwZXBtM7khyPvZ8r3fwsim3o0A==} - - fs-extra@0.30.0: - resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - - fs-extra@4.0.3: - resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} - - fs-extra@5.0.0: - resolution: {integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==} - - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs-merger@3.2.1: - resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - - fs-readdir-recursive@1.1.0: - resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} - - fs-sync@1.0.6: - resolution: {integrity: sha512-OgbfyvmGVryknZfDXVVhua6OW8946R+AF3O2xxrCW/XFxCYZ4CO2Jrl7kYhrpjZLYvB9gxvWpLikEc9YL9HzCA==} - - fs-tree-diff@0.5.9: - resolution: {integrity: sha512-872G8ax0kHh01m9n/2KDzgYwouKza0Ad9iFltBpNykvROvf2AGtoOzPJgGx125aolGPER3JuC7uZFrQ7bG1AZw==} - - fs-tree-diff@2.0.1: - resolution: {integrity: sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==} - engines: {node: 6.* || 8.* || >= 10.*} - - fs-updater@1.0.4: - resolution: {integrity: sha512-0pJX4mJF/qLsNEwTct8CdnnRdagfb+LmjRPJ8sO+nCnAZLW0cTmz4rTgU25n+RvTuWSITiLKrGVJceJPBIPlKg==} - engines: {node: '>=6.0.0'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - - function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gauge@4.0.4: - resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} - - get-intrinsic@1.2.0: - resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} - - get-source@2.0.12: - resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} - - get-stdin@4.0.1: - resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} - engines: {node: '>=0.10.0'} - - get-stream@4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} - - get-value@2.0.6: - resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} - engines: {node: '>=0.10.0'} - - git-hooks-list@1.0.3: - resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} - - git-repo-info@1.4.1: - resolution: {integrity: sha512-oqzBH6cNvE8Cq3p61ps4m0POZrVMKlARntc2BxLnuqTK+HeWpKfUMJQ7H1CvescHRINj+0a7TKA+Pp/bOq5F1Q==} - - git-repo-info@2.1.1: - resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} - engines: {node: '>= 4.0'} - - git-repo-version@1.0.2: - resolution: {integrity: sha512-OPtwtHx9E8/rTMcWT+BU6GNj6Kq/O40bHJZaZAGy+pN2RXGmeKcfr0ix4M+SQuFY8vl5L/wfPSGOAtvUT/e3Qg==} - engines: {node: ^ 4.5 || 6.* || >= 7.*} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - - glob@5.0.15: - resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} - - glob@7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - - glob@9.3.4: - resolution: {integrity: sha512-qaSc49hojMOv1EPM4EuyITjDSgSKI0rthoHnvE81tcOi1SCVndHko7auqxdQ14eiQG2NDBJBE86+2xIrbIvrbA==} - engines: {node: '>=16 || 14 >=14.17'} - - global-modules@1.0.0: - resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} - engines: {node: '>=0.10.0'} - - global-prefix@1.0.2: - resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} - engines: {node: '>=0.10.0'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} - engines: {node: '>=8'} - - globals@9.18.0: - resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} - engines: {node: '>=0.10.0'} - - globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - - globby@10.0.0: - resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} - engines: {node: '>=8'} - - globby@10.0.2: - resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} - engines: {node: '>=8'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - - gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - - got@9.6.0: - resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} - engines: {node: '>=8.6'} - - graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graceful-readlink@1.0.1: - resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} - - grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - - growly@1.3.0: - resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} - - handlebars@4.7.7: - resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-ansi@2.0.0: - resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} - engines: {node: '>=0.10.0'} - - has-ansi@3.0.0: - resolution: {integrity: sha512-5JRDTvNq6mVkaMHQVXrGnaCXHD6JfqxwCy8LA/DQSqLLqePR9uaJVm2u3Ek/UziJFQz+d1ul99RtfIhE2aorkQ==} - engines: {node: '>=4'} - - has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - - has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - - has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - - has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - - has-value@0.3.1: - resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} - engines: {node: '>=0.10.0'} - - has-value@1.0.0: - resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} - engines: {node: '>=0.10.0'} - - has-values@0.1.4: - resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} - engines: {node: '>=0.10.0'} - - has-values@1.0.0: - resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} - engines: {node: '>=0.10.0'} - - has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - - hash-for-dep@1.5.1: - resolution: {integrity: sha512-/dQ/A2cl7FBPI2pO0CANkvuuVi/IFS5oTyJ0PsOb6jW6WbVW1js5qJXMJTNbWHXBIPdFTWFbabjB+mE0d+gelw==} - - hawk@1.1.1: - resolution: {integrity: sha512-am8sVA2bCJIw8fuuVcKvmmNnGFUGW8spTkVtj2fXTEZVkfN42bwFZFtDem57eFi+NSxurJB8EQ7Jd3uCHLn8Vw==} - engines: {node: '>=0.8.0'} - deprecated: This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - heimdalljs-fs-monitor@1.1.1: - resolution: {integrity: sha512-BHB8oOXLRlrIaON0MqJSEjGVPDyqt2Y6gu+w2PaEZjrCxeVtZG7etEZp7M4ZQ80HNvnr66KIQ2lot2qdeG8HgQ==} - - heimdalljs-graph@1.0.0: - resolution: {integrity: sha512-v2AsTERBss0ukm/Qv4BmXrkwsT5x6M1V5Om6E8NcDQ/ruGkERsfsuLi5T8jx8qWzKMGYlwzAd7c/idymxRaPzA==} - engines: {node: 8.* || >= 10.*} - - heimdalljs-logger@0.1.10: - resolution: {integrity: sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==} - - heimdalljs@0.2.6: - resolution: {integrity: sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==} - - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - - hoek@0.9.1: - resolution: {integrity: sha512-ZZ6eGyzGjyMTmpSPYVECXy9uNfqBR7x5CavhUaLOeD6W0vWK1mp/b7O3f86XE0Mtfo9rZ6Bh3fnuw9Xr8MF9zA==} - engines: {node: '>=0.8.0'} - deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). - - homedir-polyfill@1.0.3: - resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} - engines: {node: '>=0.10.0'} - - hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - - hosted-git-info@6.1.1: - resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - html-encoding-sniffer@2.0.1: - resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} - engines: {node: '>=10'} - - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} - - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - - http-errors@1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - http-parser-js@0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - - http-proxy-agent@4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - - http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - - http-signature@0.10.1: - resolution: {integrity: sha512-coK8uR5rq2IMj+Hen+sKPA5ldgbCc1/spPdKCL1Fw6h+D0s/2LzMcRK0Cqufs1h0ryx/niwBHGFu8HC3hwU+lA==} - engines: {node: '>=0.8'} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - https@1.0.0: - resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} - - human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - individual@3.0.0: - resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} - - infer-owner@1.0.4: - resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - - inflection@1.13.4: - resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} - engines: {'0': node >= 0.4.0} - - inflection@2.0.1: - resolution: {integrity: sha512-wzkZHqpb4eGrOKBl34xy3umnYHx8Si5R1U4fwmdxLo5gdH6mEK8gclckTj/qWqy4Je0bsDYe/qazZYuO7xe3XQ==} - engines: {node: '>=14.0.0'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - ini@3.0.1: - resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - inline-source-map-comment@1.0.5: - resolution: {integrity: sha512-a3/m6XgooVCXkZCduOb7pkuvUtNKt4DaqaggKKJrMQHQsqt6JcJXEreExeZiiK4vWL/cM/uF6+chH05pz2/TdQ==} - hasBin: true - - inquirer@6.5.2: - resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} - engines: {node: '>=6.0.0'} - - inquirer@7.3.3: - resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} - engines: {node: '>=8.0.0'} - - inquirer@8.2.5: - resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} - engines: {node: '>=12.0.0'} - - internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} - - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - - invert-kv@3.0.1: - resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} - engines: {node: '>=8'} - - ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-accessor-descriptor@0.1.6: - resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==} - engines: {node: '>=0.10.0'} - - is-accessor-descriptor@1.0.0: - resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==} - engines: {node: '>=0.10.0'} - - is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-core-module@2.11.0: - resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} - - is-data-descriptor@0.1.4: - resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} - engines: {node: '>=0.10.0'} - - is-data-descriptor@1.0.0: - resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==} - engines: {node: '>=0.10.0'} - - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - - is-descriptor@0.1.6: - resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} - engines: {node: '>=0.10.0'} - - is-descriptor@1.0.2: - resolution: {integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==} - engines: {node: '>=0.10.0'} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extendable@1.0.1: - resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@2.0.0: - resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} - engines: {node: '>=4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-git-url@1.0.0: - resolution: {integrity: sha512-UCFta9F9rWFSavp9H3zHEHrARUfZbdJvmHKeEpds4BK3v7W2LdXoNypMtXXi5w5YBDEBCTYmbI+vsSwI8LYJaQ==} - engines: {node: '>=0.8'} - - is-glob@3.1.0: - resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - - is-lambda@1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - - is-language-code@3.1.0: - resolution: {integrity: sha512-zJdQ3QTeLye+iphMeK3wks+vXSRFKh68/Pnlw7aOfApFSEIOhYa8P9vwwa6QrImNNBMJTiL1PpYF0f4BxDuEgA==} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - - is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - - is-number@3.0.0: - resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - - is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - - is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - - is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - - is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - - is-type@0.0.1: - resolution: {integrity: sha512-YwJh/zBVrcJ90aAnPBM0CbHvm7lG9ao7lIFeqTZ1UQj4iFLpM5CikdaU+dGGesrMJwxLqPGmjjrUrQ6Kn3Zh+w==} - - is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} - - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - - is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isbinaryfile@5.0.0: - resolution: {integrity: sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==} - engines: {node: '>= 14.0.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isobject@2.1.0: - resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} - engines: {node: '>=0.10.0'} - - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - - istextorbinary@2.1.0: - resolution: {integrity: sha512-kT1g2zxZ5Tdabtpp9VSdOzW9lb6LXImyWbzbQeTxoRtHhurC9Ej9Wckngr2+uepPL09ky/mJHmN9jeJPML5t6A==} - engines: {node: '>=0.12'} - - istextorbinary@2.6.0: - resolution: {integrity: sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==} - engines: {node: '>=0.12'} - - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - - js-sdsl@4.4.0: - resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} - - js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - - js-tokens@3.0.2: - resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsdom@16.7.0: - resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} - engines: {node: '>=10'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - - jsdom@19.0.0: - resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==} - engines: {node: '>=12'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - - jsesc@0.3.0: - resolution: {integrity: sha512-UHQmAeTXV+iwEk0aHheJRqo6Or90eDxI6KIYpHSjKLXKuKlPt1CQ7tGBerFcFA8uKU5mYxiPMlckmFptd5XZzA==} - hasBin: true - - jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json-buffer@3.0.0: - resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} - - json-fn@1.1.1: - resolution: {integrity: sha512-diGeurhgiazd1lfByjn83uQkF6fVFdiCiQgJyhN3/aCl7EKye0aZe3r9eeQPKcsCh81Mntrvt46z65cn7ZwZHA==} - - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json-stable-stringify@1.0.2: - resolution: {integrity: sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==} - - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - - json-typescript@1.1.2: - resolution: {integrity: sha512-Np07MUsYMKbB0nNlw/MMIRjUK7ehO48LA4FsrzrhCfTUxMKbvOBAo0sc0b4nQ80ge9d32sModCunCgoyUojgUA==} - - json5@0.5.1: - resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} - hasBin: true - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@2.4.0: - resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} - - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - jsonify@0.0.1: - resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - - keyv@3.1.0: - resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} - - kind-of@3.2.2: - resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} - engines: {node: '>=0.10.0'} - - kind-of@4.0.0: - resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} - engines: {node: '>=0.10.0'} - - kind-of@5.1.0: - resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} - engines: {node: '>=0.10.0'} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - klaw@1.3.1: - resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} - - lcid@3.1.1: - resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==} - engines: {node: '>=8'} - - leek@0.0.24: - resolution: {integrity: sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==} - - lerna-changelog@2.2.0: - resolution: {integrity: sha512-yjYNAHrbnw8xYFKmYWJEP52Tk4xSdlNmzpYr26+3glbSGDmpe8UMo8f9DlEntjGufL+opup421oVTXcLshwAaQ==} - engines: {node: 12.* || 14.* || >= 16} - hasBin: true - - levn@0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - line-column@1.0.2: - resolution: {integrity: sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - linkify-it@1.2.4: - resolution: {integrity: sha512-eGHwtlABkp1NOJSiKUNqBf3SYAS5jPHtvRXPAgNaQwTqmkTahjtiLH9NtxdR5IOPhNvwNMN/diswSfZKzUkhGg==} - - linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} - - livereload-js@3.4.1: - resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} - - load-json-file@6.2.0: - resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} - engines: {node: '>=8'} - - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - - loader.js@4.7.0: - resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} - - locate-path@2.0.0: - resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} - engines: {node: '>=4'} - - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash._baseassign@3.2.0: - resolution: {integrity: sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==} - - lodash._basecopy@3.0.1: - resolution: {integrity: sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==} - - lodash._baseflatten@3.1.4: - resolution: {integrity: sha512-fESngZd+X4k+GbTxdMutf8ohQa0s3sJEHIcwtu4/LsIQ2JTDzdRxDCMQjW+ezzwRitLmHnacVVmosCbxifefbw==} - - lodash._bindcallback@3.0.1: - resolution: {integrity: sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==} - - lodash._createassigner@3.1.1: - resolution: {integrity: sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==} - - lodash._getnative@3.9.1: - resolution: {integrity: sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==} - - lodash._isiterateecall@3.0.9: - resolution: {integrity: sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==} - - lodash._reinterpolate@3.0.0: - resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} - - lodash.assign@3.2.0: - resolution: {integrity: sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==} - - lodash.assignin@4.2.0: - resolution: {integrity: sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==} - - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - - lodash.debounce@3.1.1: - resolution: {integrity: sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - - lodash.defaultsdeep@4.6.1: - resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==} - - lodash.find@4.6.0: - resolution: {integrity: sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==} - - lodash.flatten@3.0.2: - resolution: {integrity: sha512-jCXLoNcqQRbnT/KWZq2fIREHWeczrzpTR0vsycm96l/pu5hGeAntVBG0t7GuM/2wFqmnZs3d1eGptnAH2E8+xQ==} - - lodash.foreach@4.5.0: - resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} - - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - - lodash.isarray@3.0.4: - resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==} - - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - - lodash.keys@3.1.2: - resolution: {integrity: sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.omit@4.5.0: - resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} - - lodash.restparam@3.6.1: - resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==} - - lodash.template@4.5.0: - resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} - - lodash.templatesettings@4.2.0: - resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - - lodash.uniqby@4.7.0: - resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - log-symbols@2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - loupe@2.3.6: - resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lowercase-keys@1.0.1: - resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} - engines: {node: '>=0.10.0'} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - - magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} - engines: {node: '>=12'} - - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - make-fetch-happen@9.1.0: - resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} - engines: {node: '>= 10'} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - map-age-cleaner@0.1.3: - resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} - engines: {node: '>=6'} - - map-cache@0.2.2: - resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} - engines: {node: '>=0.10.0'} - - map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - - map-visit@1.0.0: - resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} - engines: {node: '>=0.10.0'} - - markdown-it-terminal@0.4.0: - resolution: {integrity: sha512-NeXtgpIK6jBciHTm9UhiPnyHDdqyVIdRPJ+KdQtZaf/wR74gvhCNbw5li4TYsxRp5u3ZoHEF4DwpECeZqyCw+w==} - peerDependencies: - markdown-it: '>= 13.0.0' - - markdown-it@13.0.1: - resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} - hasBin: true - - markdown-it@4.4.0: - resolution: {integrity: sha512-Rl8dHHeLuAh3E72OPY0tY7CLvlxgHiLhlshIYswAAabAg4YDBLa6e/LTgNkkxBO2K61ESzoquPQFMw/iMrT1PA==} - hasBin: true - - matcher-collection@1.1.2: - resolution: {integrity: sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==} - - matcher-collection@2.0.1: - resolution: {integrity: sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==} - engines: {node: 6.* || 8.* || >= 10.*} - - md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} - - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - - mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - - mdn-links@0.1.0: - resolution: {integrity: sha512-m+gI2Hrgro1O0SwqHd9cFkqN8VGzP56eprB63gxu6z9EFQDMeaR083wcNqMVADIbgiMP/TOCCe0ZIXHLBv2tUg==} - - mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - mem@5.1.1: - resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} - engines: {node: '>=8'} - - mem@8.1.1: - resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} - engines: {node: '>=10'} - - memory-streams@0.1.3: - resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} - - merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge-trees@2.0.0: - resolution: {integrity: sha512-5xBbmqYBalWqmhYm51XlohhkmVOua3VAUrrWh8t9iOkaLpS6ifqm/UVuUjQCeDVJ9Vx3g2l6ihfkbLSTeKsHbw==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - micromatch@3.1.10: - resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} - engines: {node: '>=0.10.0'} - - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@1.0.2: - resolution: {integrity: sha512-echfutj/t5SoTL4WZpqjA1DCud1XO0WQF3/GJ48YBmc4ZMhCK77QA6Z/w6VTQERLKuJ4drze3kw2TUT8xZXVNw==} - engines: {node: '>= 0.8.0'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@1.2.11: - resolution: {integrity: sha512-Ysa2F/nqTNGHhhm9MV8ure4+Hc+Y8AWiqUdHxsO7xu8zc92ND9f3kpALHjaP026Ft17UfxrMt95c50PLUeynBw==} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mimic-fn@1.2.0: - resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} - engines: {node: '>=4'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} - - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mini-css-extract-plugin@2.7.5: - resolution: {integrity: sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.0.1: - resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} - engines: {node: '>=10'} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - - minimatch@8.0.3: - resolution: {integrity: sha512-tEEvU9TkZgnFDCtpnrEYnPsjT7iUx42aXfs4bzmQ5sMA09/6hZY0jeZcGkXyDagiBOvkUjNo8Viom+Me6+2x7g==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@0.2.4: - resolution: {integrity: sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ==} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass-collect@1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} - - minipass-fetch@1.4.1: - resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} - engines: {node: '>=8'} - - minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - - minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - - minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - - minipass@2.9.0: - resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@4.2.5: - resolution: {integrity: sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==} - engines: {node: '>=8'} - - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - - mixin-deep@1.3.2: - resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} - engines: {node: '>=0.10.0'} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - mktemp@0.4.0: - resolution: {integrity: sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==} - engines: {node: '>0.9'} - - mocha@10.2.0: - resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} - engines: {node: '>= 14.0.0'} - hasBin: true - - morgan@1.10.0: - resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} - engines: {node: '>= 0.8.0'} - - mout@1.2.4: - resolution: {integrity: sha512-mZb9uOruMWgn/fw28DG4/yE3Kehfk1zKCLhuDU2O3vlKdnBBr4XaOCqVTflJ5aODavGUPqFHZgrFX3NJVuxGhQ==} - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - - mute-stream@0.0.7: - resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} - - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.3: - resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanomatch@1.2.13: - resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} - engines: {node: '>=0.10.0'} - - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - ndjson@2.0.0: - resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} - engines: {node: '>=10'} - hasBin: true - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - nock@13.3.0: - resolution: {integrity: sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==} - engines: {node: '>= 10.13'} - - node-fetch@2.6.9: - resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-modules-path@1.0.2: - resolution: {integrity: sha512-6Gbjq+d7uhkO7epaKi5DNgUJn7H0gEyA4Jg0Mo1uQOi3Rk50G83LtmhhFyw0LxnAFhtlspkiiw52ISP13qzcBg==} - - node-notifier@10.0.1: - resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} - - node-releases@2.0.10: - resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - - node-uuid@1.4.8: - resolution: {integrity: sha512-TkCET/3rr9mUuRp+CpO7qfgT++aAxfDRaalQhwPFzI9BY/2rCDn6OfpZOVggi1AXfTPpfkTrg5f5WQx5G1uLxA==} - deprecated: Use uuid module instead - hasBin: true - - node-watch@0.7.3: - resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} - engines: {node: '>=6'} - - nopt@3.0.6: - resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} - hasBin: true - - normalize-path@2.1.1: - resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} - engines: {node: '>=0.10.0'} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-registry-url@2.0.0: - resolution: {integrity: sha512-3e9FwDyRAhbxXw4slm4Tjv40u78yPwMc/WZkACpqNQOs5sM7wic853AeTLkMFEVhivZkclGYlse8iYsklz0Yvg==} - - normalize-url@4.5.1: - resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} - engines: {node: '>=8'} - - npm-git-info@1.0.3: - resolution: {integrity: sha512-i5WBdj4F/ULl16z9ZhsJDMl1EQCMQhHZzBwNnKL2LOA+T8IHNeRkLCVz9uVV9SzUdGTbDq+1oXhIYMe+8148vw==} - - npm-package-arg@10.1.0: - resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-run-path@2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} - - npm-run-path@3.1.0: - resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} - engines: {node: '>=8'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - npmlog@6.0.2: - resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - nwsapi@2.2.2: - resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} - - oauth-sign@0.3.0: - resolution: {integrity: sha512-Tr31Sh5FnK9YKm7xTUPyDMsNOvMqkVDND0zvK/Wgj7/H9q8mpye0qG2nVzrnsvLhcsX5DtqXD0la0ks6rkPCGQ==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-copy@0.1.0: - resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} - engines: {node: '>=0.10.0'} - - object-hash@1.3.1: - resolution: {integrity: sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==} - engines: {node: '>= 0.10.0'} - - object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object-visit@1.0.1: - resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} - engines: {node: '>=0.10.0'} - - object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} - - object.pick@1.3.0: - resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} - engines: {node: '>=0.10.0'} - - object.values@1.1.6: - resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} - engines: {node: '>= 0.4'} - - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@2.0.1: - resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} - engines: {node: '>=4'} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - optionator@0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} - - optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - - ora@3.4.0: - resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} - engines: {node: '>=6'} - - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - - os-homedir@1.0.2: - resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} - engines: {node: '>=0.10.0'} - - os-locale@5.0.0: - resolution: {integrity: sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==} - engines: {node: '>=10'} - - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - - osenv@0.1.5: - resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} - - p-cancelable@1.1.0: - resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} - engines: {node: '>=6'} - - p-defer@1.0.0: - resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} - engines: {node: '>=4'} - - p-defer@3.0.0: - resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} - engines: {node: '>=8'} - - p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} - - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - - p-finally@2.0.1: - resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} - engines: {node: '>=8'} - - p-is-promise@2.1.0: - resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} - engines: {node: '>=6'} - - p-limit@1.3.0: - resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} - engines: {node: '>=4'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@2.0.0: - resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} - engines: {node: '>=4'} - - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - - p-map@3.0.0: - resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} - engines: {node: '>=8'} - - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - - p-try@1.0.0: - resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} - engines: {node: '>=4'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-json@6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-ms@2.1.0: - resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} - engines: {node: '>=6'} - - parse-passwd@1.0.0: - resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} - engines: {node: '>=0.10.0'} - - parse-static-imports@1.1.0: - resolution: {integrity: sha512-HlxrZcISCblEV0lzXmAHheH/8qEkKgmqkdxyHTPbSqsTUV8GzqmN1L+SSti+VbNPfbBO3bYLPHDiUs2avbAdbA==} - - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - pascalcase@0.1.1: - resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} - engines: {node: '>=0.10.0'} - - path-absolute@1.0.1: - resolution: {integrity: sha512-gds5iRhSeOcDtj8gfWkRHLtZKTPsFVuh7utbjYtvnclw4XM+ffRzJrwqMhOD1PVqef7nBLmgsu1vIujjvAJrAw==} - engines: {node: '>=4'} - - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-name@1.0.0: - resolution: {integrity: sha512-/dcAb5vMXH0f51yvMuSUqFpxUcA8JelbRmE5mW/p4CUJxrNgK24IkstnV7ENtg2IDGBOu6izKTG6eilbnbNKWQ==} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-posix@1.0.0: - resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==} - - path-root-regex@0.1.2: - resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} - engines: {node: '>=0.10.0'} - - path-root@0.1.1: - resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} - engines: {node: '>=0.10.0'} - - path-scurry@1.6.3: - resolution: {integrity: sha512-RAmB+n30SlN+HnNx6EbcpoDy9nwdpcGPnEKrJnu6GZoDWBdIjo1UQMVtW2ybtC7LC2oKLcMq8y5g8WnKLiod9g==} - engines: {node: '>=16 || 14 >=14.17'} - - path-temp@2.1.0: - resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} - engines: {node: '>=8.15'} - - path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pinkie-promise@2.0.1: - resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} - engines: {node: '>=0.10.0'} - - pinkie@2.0.4: - resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} - engines: {node: '>=0.10.0'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - pkg-up@2.0.0: - resolution: {integrity: sha512-fjAPuiws93rm7mPUu21RdBnkeZNrbfCFCwfAhPWY+rR3zG0ubpe5cEReHOw5fIbfmsxEV/g2kSxGTATY3Bpnwg==} - engines: {node: '>=4'} - - pkg-up@3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} - - pnpm-sync-dependencies-meta-injected@0.0.10: - resolution: {integrity: sha512-kPcYZLaLgo5WhlgWciCJFqdxprIqaR52cY1C8KH3RGdia1YwT1wO/AOKyvOydNBJmpdcLxka2a0La9CaStxG/A==} - engines: {node: '>=16.0.0'} - hasBin: true - - portfinder@1.0.32: - resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} - engines: {node: '>= 0.12.0'} - - posix-character-classes@0.1.1: - resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} - engines: {node: '>=0.10.0'} - - postcss-modules-extract-imports@3.0.0: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.0.0: - resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.0.0: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-selector-parser@6.0.11: - resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.21: - resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prepend-http@2.0.0: - resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} - engines: {node: '>=4'} - - pretender@3.4.7: - resolution: {integrity: sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw==} - - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - - prettier@2.8.7: - resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} - engines: {node: '>=10.13.0'} - hasBin: true - - pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - - pretty-ms@7.0.1: - resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} - engines: {node: '>=10'} - - printable-characters@1.0.42: - resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - - printf@0.6.1: - resolution: {integrity: sha512-is0ctgGdPJ5951KulgfzvHGwJtZ5ck8l042vRkV6jrkpBzTmb/lueTqguWHy2JfVA+RY6gFVlaZgUS0j7S/dsw==} - engines: {node: '>= 0.9.0'} - - private@0.1.8: - resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} - engines: {node: '>= 0.6'} - - proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - process-relative-require@1.0.0: - resolution: {integrity: sha512-r8G5WJPozMJAiv8sDdVWKgJ4In/zBXqwJdMCGAXQt2Kd3HdbAuJVzWYM4JW150hWoaI9DjhtbjcsCCHIMxm8RA==} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - - promise-make-naked@2.1.2: - resolution: {integrity: sha512-y7s8ZuHIG56JYspB24be9GFkXA1zXL85Ur9u1DKrW/tvyUoPxWgBjnalK6Nc6l7wHBcAW0c3PO07+XOsWTRuhg==} - - promise-map-series@0.2.3: - resolution: {integrity: sha512-wx9Chrutvqu1N/NHzTayZjE1BgIwt6SJykQoCOic4IZ9yUDjKyVYrpLa/4YCNsV61eRENfs29hrEquVuB13Zlw==} - - promise-map-series@0.3.0: - resolution: {integrity: sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==} - engines: {node: 10.* || >= 12.*} - - promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - - promise.hash.helper@1.0.8: - resolution: {integrity: sha512-KYcnXctWUWyVD3W3Ye0ZDuA1N8Szrh85cVCxpG6xYrOk/0CttRtYCmU30nWsUch0NuExQQ63QXvzRE6FLimZmg==} - engines: {node: 10.* || >= 12.*} - - promise.prototype.finally@3.1.4: - resolution: {integrity: sha512-nNc3YbgMfLzqtqvO/q5DP6RR0SiHI9pUPGzyDf1q+usTwCN2kjvAnJkBb7bHe3o+fFSBPpsGMoYtaSi+LTNqng==} - engines: {node: '>= 0.4'} - - propagate@2.0.1: - resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} - engines: {node: '>= 8'} - - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - - proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - - pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - - punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - - punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - - qs@1.0.2: - resolution: {integrity: sha512-tHuOP9TN/1VmDM/ylApGK1QF3PSIP8I6bHDEfoKNQeViREQ/sfu1bAUrA1hoDun8p8Tpm7jcsz47g+3PiGoYdg==} - - qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} - - qs@6.11.1: - resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==} - engines: {node: '>=0.6'} - - querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - quibble@0.6.17: - resolution: {integrity: sha512-uybGnGrx1hAhBCmzmVny+ycKaS5F71+q+iWVzbf8x/HyeEMDGeiQFVjWl1zhi4rwfTHa05+/NIExC4L5YRNPjQ==} - engines: {node: '>= 0.14.0'} - - quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - - quick-temp@0.1.8: - resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==} - - qunit-console-grouper@0.3.0: - resolution: {integrity: sha512-uHg5kcjyEGX85Rh9mimWoXsqeGmJZxfNLqmVxA5O2xJW+JAlQ0r0JFA07lHqlOQcegs5CAhvioKUXpjicDra6g==} - engines: {node: 10.* || 12.* || >= 14.*} - - qunit-dom@2.0.0: - resolution: {integrity: sha512-mElzLN99wYPOGekahqRA+mq7NcThXY9c+/tDkgJmT7W5LeZAFNyITr2rFKNnCbWLIhuLdFw88kCBMrJSfyBYpA==} - engines: {node: 12.* || 14.* || >= 16.*} - - qunit@2.19.4: - resolution: {integrity: sha512-aqUzzUeCqlleWYKlpgfdHHw9C6KxkB9H3wNfiBg5yHqQMzy0xw/pbCRHYFkjl8MsP/t8qkTQE+JTYL71azgiew==} - engines: {node: '>=10'} - hasBin: true - - rambda@7.5.0: - resolution: {integrity: sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@1.1.7: - resolution: {integrity: sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==} - engines: {node: '>= 0.8.0'} - - raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} - - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - read-ini-file@4.0.0: - resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==} - engines: {node: '>=14.6'} - - read-yaml-file@2.1.0: - resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} - engines: {node: '>=10.13'} - - readable-stream@1.0.34: - resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - realpath-missing@1.1.0: - resolution: {integrity: sha512-wnWtnywepjg/eHIgWR97R7UuM5i+qHLA195qdN9UPKvcMqfn60+67S8sPPW3vDlSEfYHoFkKU8IvpCNty3zQvQ==} - engines: {node: '>=10'} - - recast@0.18.10: - resolution: {integrity: sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==} - engines: {node: '>= 4'} - - recast@0.19.1: - resolution: {integrity: sha512-8FCjrBxjeEU2O6I+2hyHyBFH1siJbMBLwIRvVr1T3FD2cL754sOaJDsJ/8h3xYltasbJ8jqWRIhMuDGBSiSbjw==} - engines: {node: '>= 4'} - - redeyed@1.0.1: - resolution: {integrity: sha512-8eEWsNCkV2rvwKLS1Cvp5agNjMhwRe2um+y32B2+3LqOzg4C9BBPs6vzAfV16Ivb8B9HPNKIqd8OrdBws8kNlQ==} - - regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - - regenerator-runtime@0.11.1: - resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} - - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - regenerator-transform@0.10.1: - resolution: {integrity: sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==} - - regenerator-transform@0.15.2: - resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - - regex-not@1.0.2: - resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} - engines: {node: '>=0.10.0'} - - regexp.prototype.flags@1.4.3: - resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} - engines: {node: '>= 0.4'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - regexpu-core@2.0.0: - resolution: {integrity: sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==} - - regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} - - registry-auth-token@4.2.2: - resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} - engines: {node: '>=6.0.0'} - - registry-url@5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - - regjsgen@0.2.0: - resolution: {integrity: sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==} - - regjsparser@0.1.5: - resolution: {integrity: sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==} - hasBin: true - - regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true - - remote-git-tags@3.0.0: - resolution: {integrity: sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==} - engines: {node: '>=8'} - - remove-trailing-separator@1.1.0: - resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} - - remove-types@1.0.0: - resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} - - repeat-element@1.1.4: - resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} - engines: {node: '>=0.10.0'} - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - request@2.40.0: - resolution: {integrity: sha512-waNoGB4Z7bPn+lgqPk7l7hhze4Vd68jKccnwLeS7vr9GMxz0iWQbYTbBNWzfIk87Urx7V44pu29qjF/omej+Fw==} - engines: {'0': node >= 0.8.0} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - requireindex@1.2.0: - resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} - engines: {node: '>=0.10.5'} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - reselect@3.0.1: - resolution: {integrity: sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==} - - reselect@4.1.7: - resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==} - - resolve-dir@1.0.1: - resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-package-path@1.2.7: - resolution: {integrity: sha512-fVEKHGeK85bGbVFuwO9o1aU0n3vqQGrezPc51JGu9UTXpFQfWq5qCeKxyaRUSvephs+06c5j5rPq/dzHGEo8+Q==} - - resolve-package-path@2.0.0: - resolution: {integrity: sha512-/CLuzodHO2wyyHTzls5Qr+EFeG6RcW4u6//gjYvUfcfyuplIX1SSccU+A5A9A78Gmezkl3NBkFAMxLbzTY9TJA==} - engines: {node: 8.* || 10.* || >= 12} - - resolve-package-path@3.1.0: - resolution: {integrity: sha512-2oC2EjWbMJwvSN6Z7DbDfJMnD8MYEouaLn5eIX0j8XwPsYCVIyY9bbnX88YHVkbr8XHqvZrYbxaLPibfTYKZMA==} - engines: {node: 10.* || >= 12} - - resolve-package-path@4.0.3: - resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} - engines: {node: '>= 12'} - - resolve-path@1.4.0: - resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} - engines: {node: '>= 0.8'} - - resolve-url@0.2.1: - resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} - deprecated: https://github.com/lydell/resolve-url#deprecated - - resolve@1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - - responselike@1.0.2: - resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} - - restore-cursor@2.0.0: - resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} - engines: {node: '>=4'} - - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - - ret@0.1.15: - resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} - engines: {node: '>=0.12'} - - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - right-pad@1.0.1: - resolution: {integrity: sha512-bYBjgxmkvTAfgIYy328fmkwhp39v8lwVgWhhrzxPV3yHtcSqyYKe9/XOhvW48UFjATg3VuJbpsp5822ACNvkmw==} - engines: {node: '>= 0.10'} - deprecated: Please use String.prototype.padEnd() over this package. - - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - hasBin: true - - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - - rimraf@4.4.1: - resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} - engines: {node: '>=14'} - hasBin: true - - rollup-plugin-copy-assets@2.0.3: - resolution: {integrity: sha512-ETShhQGb9SoiwcNrvb3BhUNSGR89Jao0+XxxfzzLW1YsUzx8+rMO4z9oqWWmo6OHUmfNQRvqRj0cAyPkS9lN9w==} - peerDependencies: - rollup: '>=1.1.2' - - rollup-plugin-delete@2.0.0: - resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==} - engines: {node: '>=10'} - - rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - - rollup@2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} - hasBin: true - - rollup@3.20.2: - resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.17.2: - resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - route-recognizer@0.3.4: - resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} - - router_js@8.0.5: - resolution: {integrity: sha512-0TpJIJoOpPVlX3JIGAQd/vivCXWkoi6wTAM7CkYo2cuASCQsK4qtJ9pvzYki7iZw44hO6nRN3z6paVTMiAPLdw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - route-recognizer: ^0.3.4 - rsvp: ^4.8.5 - - rsvp@3.2.1: - resolution: {integrity: sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==} - - rsvp@3.6.2: - resolution: {integrity: sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==} - engines: {node: 0.12.* || 4.* || 6.* || >= 7.*} - - rsvp@4.8.5: - resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} - engines: {node: 6.* || >= 7.*} - - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - rxjs@6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} - - rxjs@7.8.0: - resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} - - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-execa@0.1.2: - resolution: {integrity: sha512-vdTshSQ2JsRCgT8eKZWNJIL26C6bVqy1SOmuCMlKHegVeo8KYRobRrefOdUq9OozSPUUiSxrylteeRmLOMFfWg==} - engines: {node: '>=12'} - - safe-json-parse@1.0.1: - resolution: {integrity: sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==} - - safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - - safe-regex@1.1.0: - resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} - - safe-stable-stringify@2.4.3: - resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} - engines: {node: '>=10'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sane@4.1.0: - resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} - engines: {node: 6.* || 8.* || >= 10.*} - deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added - hasBin: true - - sane@5.0.1: - resolution: {integrity: sha512-9/0CYoRz0MKKf04OMCO3Qk3RQl1PAwWAhPSQSym4ULiLpTZnrY1JoZU0IEikHu8kdk2HvKT/VwQMq/xFZ8kh1Q==} - engines: {node: 10.* || >= 12.*} - hasBin: true - - saxes@5.0.1: - resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} - engines: {node: '>=10'} - - schema-utils@2.7.1: - resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} - engines: {node: '>= 8.9.0'} - - schema-utils@3.1.1: - resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} - engines: {node: '>= 10.13.0'} - - schema-utils@4.0.0: - resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==} - engines: {node: '>= 12.13.0'} - - semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - - semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - - semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true - - send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} - - serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - - serialize-javascript@6.0.1: - resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} - - serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} - - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - - set-value@2.0.1: - resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} - engines: {node: '>=0.10.0'} - - setprototypeof@1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shellwords@0.1.1: - resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} - - side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - silent-error@1.1.1: - resolution: {integrity: sha512-n4iEKyNcg4v6/jpb3c0/iyH2G1nzUNl7Gpqtn/mHIJK9S/q/7MCfoO4rwVOoO59qPFIc0hVHvMbiOJ0NdtxKKw==} - - simple-dom@1.4.0: - resolution: {integrity: sha512-TnBPkmOyjdaOqyBMb4ick+n8c0Xv9Iwg1PykFV7hz9Se3UCiacTbRb+25cPmvozFNJLBUNvUzX/KsPfXF14ivA==} - - simple-html-tokenizer@0.5.11: - resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} - - slash@2.0.0: - resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} - engines: {node: '>=6'} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - - snapdragon-node@2.1.1: - resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} - engines: {node: '>=0.10.0'} - - snapdragon-util@3.0.1: - resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} - engines: {node: '>=0.10.0'} - - snapdragon@0.8.2: - resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} - engines: {node: '>=0.10.0'} - - sntp@0.2.4: - resolution: {integrity: sha512-bDLrKa/ywz65gCl+LmOiIhteP1bhEsAAzhfMedPoiHP3dyYnAevlaJshdqb9Yu0sRifyP/fRqSt8t+5qGIWlGQ==} - engines: {node: '>=0.8.0'} - deprecated: This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. - - socket.io-adapter@2.5.2: - resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} - - socket.io-parser@4.2.2: - resolution: {integrity: sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==} - engines: {node: '>=10.0.0'} - - socket.io@4.6.1: - resolution: {integrity: sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==} - engines: {node: '>=10.0.0'} - - socks-proxy-agent@6.2.1: - resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} - engines: {node: '>= 10'} - - socks@2.7.1: - resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} - engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} - - sort-keys@4.2.0: - resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==} - engines: {node: '>=8'} - - sort-object-keys@1.1.3: - resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - - sort-package-json@1.57.0: - resolution: {integrity: sha512-FYsjYn2dHTRb41wqnv+uEqCUvBpK3jZcTp9rbz2qDTmel7Pmdtf+i2rLaaPMRZeSVM60V3Se31GyWFpmKs4Q5Q==} - hasBin: true - - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - - source-map-resolve@0.5.3: - resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map-url@0.3.0: - resolution: {integrity: sha512-QU4fa0D6aSOmrT+7OHpUXw+jS84T0MLaQNtFs8xzLNe6Arj44Magd7WEbyVW5LNYoAPVV35aKs4azxIfVJrToQ==} - deprecated: See https://github.com/lydell/source-map-url#deprecated - - source-map-url@0.4.1: - resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} - deprecated: See https://github.com/lydell/source-map-url#deprecated - - source-map@0.1.43: - resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} - engines: {node: '>=0.8.0'} - - source-map@0.4.4: - resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} - engines: {node: '>=0.8.0'} - - source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - - sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - - sourcemap-validator@1.1.1: - resolution: {integrity: sha512-pq6y03Vs6HUaKo9bE0aLoksAcpeOo9HZd7I8pI6O480W/zxNZ9U32GfzgtPP0Pgc/K1JHna569nAbOk3X8/Qtw==} - engines: {node: ^0.10 || ^4.5 || 6.* || >= 7.*} - - spawn-args@0.2.0: - resolution: {integrity: sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==} - - split-string@3.1.0: - resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} - engines: {node: '>=0.10.0'} - - split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - sprintf-js@1.1.2: - resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} - - sri-toolbox@0.2.0: - resolution: {integrity: sha512-DQIMWCAr/M7phwo+d3bEfXwSBEwuaJL+SJx9cuqt1Ty7K96ZFoHpYnSbhrQZEr0+0/GtmpKECP8X/R4RyeTAfw==} - engines: {node: '>= 0.10.4'} - - ssri@8.0.1: - resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} - engines: {node: '>= 8'} - - stacktracey@2.1.8: - resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} - - stagehand@1.0.1: - resolution: {integrity: sha512-GqXBq2SPWv9hTXDFKS8WrKK1aISB0aKGHZzH+uD4ShAgs+Fz20ZfoerLOm8U+f62iRWLrw6nimOY/uYuTcVhvg==} - engines: {node: 6.* || 8.* || >= 10.*} - - static-extend@0.1.2: - resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} - engines: {node: '>=0.10.0'} - - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - - string-template@0.2.1: - resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} - - string-width@2.1.1: - resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} - engines: {node: '>=4'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string.prototype.matchall@4.0.8: - resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} - - string.prototype.trim@1.2.7: - resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.6: - resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} - - string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} - - string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - stringify-object-es5@2.5.0: - resolution: {integrity: sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==} - engines: {node: '>=0.10.0'} - - stringstream@0.0.6: - resolution: {integrity: sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==} - - strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} - - strip-ansi@4.0.0: - resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} - engines: {node: '>=4'} - - strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-comments-strings@1.2.0: - resolution: {integrity: sha512-zwF4bmnyEjZwRhaak9jUWNxc0DoeKBJ7lwSN/LEc8dQXZcUFG6auaaTQJokQWXopLdM3iTx01nQT8E4aL29DAQ==} - - strip-eof@1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - stubborn-fs@1.2.5: - resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} - - style-loader@2.0.0: - resolution: {integrity: sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - - styled_string@0.0.1: - resolution: {integrity: sha512-DU2KZiB6VbPkO2tGSqQ9n96ZstUPjW7X4sGO6V2m1myIQluX0p1Ol8BrA/l6/EesqhMqXOIXs3cJNOy1UuU2BA==} - - sum-up@1.0.3: - resolution: {integrity: sha512-zw5P8gnhiqokJUWRdR6F4kIIIke0+ubQSGyYUY506GCbJWtV7F6Xuy0j6S125eSX2oF+a8KdivsZ8PlVEH0Mcw==} - - supports-color@2.0.0: - resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} - engines: {node: '>=0.8.0'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-color@9.4.0: - resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} - engines: {node: '>=12'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - - symlink-or-copy@1.3.1: - resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} - - sync-disk-cache@1.3.4: - resolution: {integrity: sha512-GlkGeM81GPPEKz/lH7QUTbvqLq7K/IUTuaKDSMulP9XQ42glqNJIN/RKgSOw4y8vxL1gOVvj+W7ruEO4s36eCw==} - - sync-disk-cache@2.1.0: - resolution: {integrity: sha512-vngT2JmkSapgq0z7uIoYtB9kWOOzMihAAYq/D3Pjm/ODOGMgS4r++B+OZ09U4hWR6EaOdy9eqQ7/8ygbH3wehA==} - engines: {node: 8.* || >= 10.*} - - tap-parser@7.0.0: - resolution: {integrity: sha512-05G8/LrzqOOFvZhhAk32wsGiPZ1lfUrl+iV7+OkKgfofZxiceZWMHkKmow71YsyVQ8IvGBP2EjcIjE5gL4l5lA==} - hasBin: true - - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - - tar@6.1.13: - resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} - engines: {node: '>=10'} - - temp@0.9.4: - resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} - engines: {node: '>=6.0.0'} - - terser-webpack-plugin@5.3.7: - resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - - terser@5.16.8: - resolution: {integrity: sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==} - engines: {node: '>=10'} - hasBin: true - - testdouble@3.17.1: - resolution: {integrity: sha512-6u3v0rKHf3oJHSD7UuxFkXXcvV3QWguYdfU9QKaRjLF/wRa9Hxg0fmRzdbMhPX2fYs4xVnw4l1h2GsKJiwFRlw==} - engines: {node: '>= 14'} - - testem@3.10.1: - resolution: {integrity: sha512-42c4e7qlAelwMd8O3ogtVGRbgbr6fJnX6H51ACOIG1V1IjsKPlcQtxPyOwaL4iikH22Dfh+EyIuJnMG4yxieBQ==} - engines: {node: '>= 7.*'} - hasBin: true - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - textextensions@2.6.0: - resolution: {integrity: sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==} - engines: {node: '>=0.8'} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - theredoc@1.0.0: - resolution: {integrity: sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==} - - thread-loader@3.0.4: - resolution: {integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.27.0 || ^5.0.0 - - through2@3.0.2: - resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} - - through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} - - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - - tiny-lr@2.0.0: - resolution: {integrity: sha512-f6nh0VMRvhGx4KCeK1lQ/jaL0Zdb5WdR+Jk8q9OSUQnaSDxAEGH1fgqLZ+cMl5EW3F2MGnCsalBO1IsnnogW1Q==} - - tiny-readdir@2.7.2: - resolution: {integrity: sha512-211Pbj4W3EVVIrIkABDPlEyLNzAz1Zb921qwmkKQvx7YR90ma3wuzojFx62nptlrAlI/ict1f++r9E/+9DcWnQ==} - - tmp-sync@1.1.2: - resolution: {integrity: sha512-npRDYJiMaPWhcLf6q06v/vA3o/ZG4hfHDiBuj1N3Yeh3GTkFQb1YLFs6inDGMWIHjGidl4Oc1+oXHNKKj5vkDQ==} - engines: {node: '>=0.8.0'} - - tmp@0.0.28: - resolution: {integrity: sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==} - engines: {node: '>=0.4.0'} - - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.1.0: - resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} - engines: {node: '>=6'} - - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - - to-fast-properties@1.0.3: - resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} - engines: {node: '>=0.10.0'} - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - to-object-path@0.3.0: - resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} - engines: {node: '>=0.10.0'} - - to-readable-stream@1.0.0: - resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} - engines: {node: '>=6'} - - to-regex-range@2.1.1: - resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} - engines: {node: '>=0.10.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - to-regex@3.0.2: - resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} - engines: {node: '>=0.10.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - tough-cookie@4.1.2: - resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} - engines: {node: '>=6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - tr46@2.1.0: - resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} - engines: {node: '>=8'} - - tr46@3.0.0: - resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} - engines: {node: '>=12'} - - tree-sync@1.4.0: - resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} - - tree-sync@2.1.0: - resolution: {integrity: sha512-OLWW+Nd99NOM53aZ8ilT/YpEiOo6mXD3F4/wLbARqybSZ3Jb8IxHK5UGVbZaae0wtXAyQshVV+SeqVBik+Fbmw==} - engines: {node: '>=8'} - - tsconfig-paths@3.14.2: - resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} - - tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - - tslib@2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - - tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - - tunnel-agent@0.4.3: - resolution: {integrity: sha512-e0IoVDWx8SDHc/hwFTqJDQ7CCDTEeGhmcT9jkWJjoGQSpgBz20nAMr80E3Tpk7PatJ1b37DQDgJR3CNSzcMOZQ==} - - type-check@0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-detect@0.1.1: - resolution: {integrity: sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==} - - type-detect@1.0.0: - resolution: {integrity: sha512-f9Uv6ezcpvCQjJU0Zqbg+65qdcszv3qUQsZfjdRbWiZ7AMenrX1u0lNk9EoWWX6e1F+NULyg27mtdeZ5WhpljA==} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.11.0: - resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} - engines: {node: '>=8'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - - typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - - typescript-memoize@1.1.1: - resolution: {integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==} - - typescript@5.0.3: - resolution: {integrity: sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==} - engines: {node: '>=12.20'} - hasBin: true - - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - - typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - - uc.micro@1.0.6: - resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - - uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} - engines: {node: '>=0.8.0'} - hasBin: true - - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - - underscore.string@3.3.6: - resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} - - underscore@1.13.6: - resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} - - unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - - union-value@1.0.1: - resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} - engines: {node: '>=0.10.0'} - - unique-filename@1.1.1: - resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} - - unique-slug@2.0.2: - resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} - - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + tests/fastboot: + dependencies: + '@ember-data/unpublished-test-infra': + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@3.1.1)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-inflector@4.0.3)(ember-source@5.12.0)(qunit@2.19.4) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + webpack: + specifier: 5.94.0 + version: 5.94.0 + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/runtime': + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/string': + specifier: ^3.1.1 + version: 3.1.1(@babel/core@7.26.0) + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-dependency-checker: + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-fastboot: + specifier: ^4.1.5 + version: 4.1.5(@babel/core@7.26.0)(ember-cli@5.12.0)(ember-source@5.12.0) + ember-cli-fastboot-testing: + specifier: ^0.6.2 + version: 0.6.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-fastboot@4.1.5)(ember-cli@5.12.0)(ember-source@5.12.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-cli-inject-live-reload: + specifier: ^2.1.0 + version: 2.1.0 + ember-cli-version-checker: + specifier: ^5.1.2 + version: 5.1.2 + ember-inflector: + specifier: 4.0.3 + version: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) + ember-load-initializers: + specifier: ^2.1.2 + version: 2.1.2(@babel/core@7.26.0) + ember-maybe-import-regenerator: + specifier: ^1.0.0 + version: 1.0.0(@babel/core@7.26.0) + ember-qunit: + specifier: 8.0.2 + version: 8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4) + ember-resolver: + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-simple-tree: + specifier: ^0.8.4 + version: 0.8.4(@babel/core@7.26.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + qunit-dom: + specifier: ^3.1.1 + version: 3.3.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + dependenciesMeta: + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@ember-data/unpublished-test-infra': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + ember-data: + injected: true - universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} + tests/full-data-asset-size-app: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/runtime': + specifier: ^7.24.5 + version: 7.26.0 + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-dependency-checker: + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-cli-terser: + specifier: ^4.0.2 + version: 4.0.2 + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-source@5.12.0) + ember-load-initializers: + specifier: ^2.1.2 + version: 2.1.2(@babel/core@7.26.0) + ember-maybe-import-regenerator: + specifier: ^1.0.0 + version: 1.0.0(@babel/core@7.26.0) + ember-resolver: + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + webpack: + specifier: 5.94.0 + version: 5.94.0 + zlib: + specifier: 1.0.5 + version: 1.0.5 + dependenciesMeta: + ember-data: + injected: true - universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} + tests/main: + dependencies: + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/plugin-transform-typescript': + specifier: ^7.24.5 + version: 7.25.9(@babel/core@7.26.0) + '@babel/runtime': + specifier: ^7.24.5 + version: 7.26.0 + '@ember-data/adapter': + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/unpublished-test-infra': + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': + specifier: ^1.2.0 + version: 1.2.0 + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/string': + specifier: ^3.1.1 + version: 3.1.1(@babel/core@7.26.0) + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/macros': + specifier: ^1.16.6 + version: 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@glint/core': + specifier: 1.5.0 + version: 1.5.0(typescript@5.6.3) + '@glint/environment-ember-loose': + specifier: 1.5.0 + version: 1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0) + '@glint/environment-ember-template-imports': + specifier: 1.5.0 + version: 1.5.0(@glint/environment-ember-loose@1.5.0)(@glint/template@1.5.0) + '@glint/template': + specifier: 1.5.0 + version: 1.5.0 + '@types/qunit': + specifier: 2.19.10 + version: 2.19.10 + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/holodeck': + specifier: workspace:* + version: file:packages/holodeck(@ember-data/request@4.12.8)(@warp-drive/core-types@4.12.8) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + broccoli-concat: + specifier: ^4.2.5 + version: 4.2.5 + broccoli-merge-trees: + specifier: ^4.2.0 + version: 4.2.0 + broccoli-stew: + specifier: ^3.0.0 + version: 3.0.0 + broccoli-string-replace: + specifier: ^0.1.2 + version: 0.1.2 + broccoli-test-helper: + specifier: ^2.0.0 + version: 2.0.0 + broccoli-uglify-sourcemap: + specifier: ^4.0.0 + version: 4.0.0 + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-cached-decorator-polyfill: + specifier: ^1.0.2 + version: 1.0.2(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-dependency-checker: + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-cli-inject-live-reload: + specifier: ^2.1.0 + version: 2.1.0 + ember-cli-terser: + specifier: ~4.0.2 + version: 4.0.2 + ember-cli-test-loader: + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@3.1.1)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(ember-inflector@4.0.3)(ember-source@5.12.0)(qunit@2.19.4) + ember-decorators-polyfill: + specifier: ^1.1.5 + version: 1.1.5(@babel/core@7.26.0) + ember-disable-prototype-extensions: + specifier: ^1.1.3 + version: 1.1.3 + ember-exam: + specifier: ^9.0.0 + version: 9.0.0(@glint/template@1.5.0)(ember-qunit@8.0.2)(ember-source@5.12.0)(qunit@2.19.4) + ember-inflector: + specifier: 4.0.3 + version: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) + ember-load-initializers: + specifier: ^2.1.2 + version: 2.1.2(@babel/core@7.26.0) + ember-maybe-import-regenerator: + specifier: ^1.0.0 + version: 1.0.0(@babel/core@7.26.0) + ember-qunit: + specifier: 8.0.2 + version: 8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4) + ember-resolver: + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + ember-source-channel-url: + specifier: ^3.0.0 + version: 3.0.0 + ember-strict-resolver: + specifier: ^1.3.0 + version: 1.3.0(@babel/core@7.26.0) + ember-template-imports: + specifier: 4.1.3 + version: 4.1.3 + ember-try: + specifier: ^3.0.0 + version: 3.0.0 + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + pretender: + specifier: ^3.4.7 + version: 3.4.7 + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + qunit-dom: + specifier: ^3.1.1 + version: 3.3.0 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + webpack: + specifier: 5.94.0 + version: 5.94.0 + dependenciesMeta: + '@ember-data/adapter': + injected: true + '@ember-data/debug': + injected: true + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/serializer': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@ember-data/unpublished-test-infra': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + '@warp-drive/holodeck': + injected: true + ember-data: + injected: true - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} + tests/performance: + dependencies: + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-source@5.12.0) + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + webpack: + specifier: 5.94.0 + version: 5.94.0 + devDependencies: + '@babel/core': + specifier: ^7.24.5 + version: 7.26.0 + '@babel/runtime': + specifier: ^7.24.5 + version: 7.26.0 + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/test-helpers': + specifier: 4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/compat': + specifier: ^3.5.3 + version: 3.7.0(@embroider/core@3.4.19) + '@embroider/core': + specifier: ^3.4.12 + version: 3.4.19 + '@embroider/webpack': + specifier: ^4.0.3 + version: 4.0.8(@embroider/core@3.4.19)(webpack@5.94.0) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-dependency-checker: + specifier: ^3.3.2 + version: 3.3.2(ember-cli@5.12.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-load-initializers: + specifier: ^2.1.2 + version: 2.1.2(@babel/core@7.26.0) + ember-maybe-import-regenerator: + specifier: ^1.0.0 + version: 1.0.0(@babel/core@7.26.0) + ember-resolver: + specifier: ^11.0.1 + version: 11.0.1(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + terser-webpack-plugin: + specifier: ^5.3.10 + version: 5.3.10(webpack@5.94.0) + zlib: + specifier: 1.0.5 + version: 1.0.5 + dependenciesMeta: + ember-data: + injected: true - unset-value@1.0.0: - resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} - engines: {node: '>=0.10.0'} + tests/vite-basic-compat: + devDependencies: + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/eslint-parser': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0)(eslint@9.14.0) + '@babel/plugin-transform-runtime': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/runtime': + specifier: ^7.26.0 + version: 7.26.0 + '@ember-data/adapter': + specifier: workspace:* + version: file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': + specifier: workspace:* + version: file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': + specifier: workspace:* + version: file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': + specifier: workspace:* + version: file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': + specifier: workspace:* + version: file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': + specifier: workspace:* + version: file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': + specifier: workspace:* + version: file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': + specifier: workspace:* + version: file:packages/request-utils(@babel/core@7.26.0)(@ember/string@4.0.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/serializer': + specifier: workspace:* + version: file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': + specifier: workspace:* + version: file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': + specifier: workspace:* + version: file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/unpublished-test-infra': + specifier: workspace:* + version: file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0) + '@ember/optional-features': + specifier: ^2.1.0 + version: 2.2.0 + '@ember/string': + specifier: ^4.0.0 + version: 4.0.0 + '@ember/test-helpers': + specifier: ^4.0.4 + version: 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': + specifier: ^3.1.0 + version: 3.1.0(@babel/core@7.26.0) + '@embroider/compat': + specifier: 3.7.1-unstable.4070ba7 + version: 3.7.1-unstable.4070ba7(@embroider/core@3.4.20-unstable.4070ba7)(@glimmer/component@1.1.2) + '@embroider/config-meta-loader': + specifier: 0.0.1-unstable.4070ba7 + version: 0.0.1-unstable.4070ba7 + '@embroider/core': + specifier: 3.4.20-unstable.4070ba7 + version: 3.4.20-unstable.4070ba7 + '@embroider/test-setup': + specifier: 4.0.1-unstable.4070ba7 + version: 4.0.1-unstable.4070ba7(@embroider/compat@3.7.1-unstable.4070ba7)(@embroider/core@3.4.20-unstable.4070ba7) + '@embroider/vite': + specifier: 0.2.2-unstable.4070ba7 + version: 0.2.2-unstable.4070ba7(@embroider/core@3.4.20-unstable.4070ba7)(vite@5.4.11) + '@glimmer/component': + specifier: ^1.1.2 + version: 1.1.2(@babel/core@7.26.0) + '@glimmer/tracking': + specifier: ^1.1.2 + version: 1.1.2 + '@rollup/plugin-babel': + specifier: ^6.0.4 + version: 6.0.4(@babel/core@7.26.0)(rollup@4.25.0) + '@tsconfig/ember': + specifier: ^3.0.8 + version: 3.0.8 + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 + '@types/qunit': + specifier: 2.19.10 + version: 2.19.10 + '@types/rsvp': + specifier: ^4.0.9 + version: 4.0.9 + '@typescript-eslint/eslint-plugin': + specifier: ^8.14.0 + version: 8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.14.0 + version: 8.14.0(eslint@9.14.0)(typescript@5.6.3) + '@warp-drive/build-config': + specifier: workspace:* + version: file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': + specifier: workspace:* + version: file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/internal-config': + specifier: workspace:* + version: link:../../config + babel-plugin-ember-template-compilation: + specifier: ^2.3.0 + version: 2.3.0 + concurrently: + specifier: ^9.1.0 + version: 9.1.0 + decorator-transforms: + specifier: ^2.3.0 + version: 2.3.0(@babel/core@7.26.0) + ember-auto-import: + specifier: ^2.10.0 + version: 2.10.0(@glint/template@1.5.0) + ember-cli: + specifier: ~5.12.0 + version: 5.12.0 + ember-cli-babel: + specifier: ^8.2.0 + version: 8.2.0(@babel/core@7.26.0) + ember-cli-htmlbars: + specifier: ^6.3.0 + version: 6.3.0 + ember-data: + specifier: workspace:* + version: file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@4.0.0)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-source@5.12.0)(qunit@2.19.4) + ember-load-initializers: + specifier: ^3.0.1 + version: 3.0.1(ember-source@5.12.0) + ember-modifier: + specifier: ^4.2.0 + version: 4.2.0(@babel/core@7.26.0)(ember-source@5.12.0) + ember-page-title: + specifier: ^8.2.3 + version: 8.2.3(@glimmer/component@1.1.2)(ember-source@5.12.0) + ember-qunit: + specifier: 9.0.1 + version: 9.0.1(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-source@5.12.0)(qunit@2.19.4) + ember-resolver: + specifier: ^13.1.0 + version: 13.1.0(@babel/core@7.26.0)(ember-source@5.12.0) + ember-route-template: + specifier: ^1.0.3 + version: 1.0.3 + ember-source: + specifier: ~5.12.0 + version: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + ember-template-lint: + specifier: ^6.0.0 + version: 6.0.0 + ember-welcome-page: + specifier: ^7.0.2 + version: 7.0.2 + eslint: + specifier: ^9.14.0 + version: 9.14.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.14.0) + eslint-plugin-ember: + specifier: ^12.3.1 + version: 12.3.1(@babel/core@7.26.0)(@typescript-eslint/parser@8.14.0)(eslint@9.14.0) + eslint-plugin-n: + specifier: ^17.13.1 + version: 17.13.1(eslint@9.14.0) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.2.1(eslint-config-prettier@9.1.0)(eslint@9.14.0)(prettier@3.3.3) + eslint-plugin-qunit: + specifier: ^8.1.2 + version: 8.1.2(eslint@9.14.0) + globals: + specifier: ^15.12.0 + version: 15.12.0 + loader.js: + specifier: ^4.7.0 + version: 4.7.0 + pnpm-sync-dependencies-meta-injected: + specifier: 0.0.14 + version: 0.0.14 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + prettier-plugin-ember-template-tag: + specifier: ^2.0.4 + version: 2.0.4(prettier@3.3.3) + qunit: + specifier: 2.19.4 + version: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + qunit-dom: + specifier: ^3.3.0 + version: 3.3.0 + tracked-built-ins: + specifier: ^3.3.0 + version: 3.3.0(@babel/core@7.26.0) + typescript: + specifier: ^5.5.4 + version: 5.6.3 + typescript-eslint: + specifier: ^8.13.0 + version: 8.14.0(eslint@9.14.0)(typescript@5.6.3) + vite: + specifier: ^5.4.11 + version: 5.4.11(@types/node@20.17.6) + webpack: + specifier: 5.94.0 + version: 5.94.0 + dependenciesMeta: + '@ember-data/adapter': + injected: true + '@ember-data/debug': + injected: true + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/legacy-compat': + injected: true + '@ember-data/model': + injected: true + '@ember-data/request': + injected: true + '@ember-data/request-utils': + injected: true + '@ember-data/serializer': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true + '@ember-data/unpublished-test-infra': + injected: true + '@warp-drive/build-config': + injected: true + '@warp-drive/core-types': + injected: true + ember-data: + injected: true - untildify@2.1.0: - resolution: {integrity: sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==} - engines: {node: '>=0.10.0'} +packages: - update-browserslist-db@1.0.10: - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 - update-browserslist-db@1.0.13: - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /@babel/cli@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-I+02IfrTiSanpxJBlZQYb18qCxB6c2Ih371cVpfgIrPQrjAYkf45XxomTJOG8JBWX5GY35/+TmhCMdJ4ZPkL8Q==} + engines: {node: '>=6.9.0'} hasBin: true peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - urix@0.1.0: - resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} - deprecated: Please see https://github.com/lydell/urix#deprecated - - url-parse-lax@3.0.0: - resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} - engines: {node: '>=4'} - - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - - url@0.11.0: - resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} - - use@3.1.1: - resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} - engines: {node: '>=0.10.0'} - - username-sync@1.0.3: - resolution: {integrity: sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@jridgewell/trace-mapping': 0.3.25 + commander: 6.2.1 + convert-source-map: 2.0.0 + fs-readdir-recursive: 1.1.0 + glob: 7.2.3 + make-dir: 2.1.0 + slash: 2.0.0 + optionalDependencies: + '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 + chokidar: 3.6.0 + dev: false - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true + /@babel/code-frame@7.26.2: + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 - validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + /@babel/compat-data@7.26.2: + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} - validate-peer-dependencies@1.2.0: - resolution: {integrity: sha512-nd2HUpKc6RWblPZQ2GDuI65sxJ2n/UqZwSBVtj64xlWjMx0m7ZB2m9b2JS3v1f+n9VWH/dd1CMhkHfP6pIdckA==} + /@babel/core@7.26.0: + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - validate-peer-dependencies@2.2.0: - resolution: {integrity: sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA==} - engines: {node: '>= 12'} + /@babel/core@7.26.0(supports-color@8.1.1): + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + /@babel/eslint-parser@7.25.8(@babel/core@7.26.0)(eslint@9.14.0): + resolution: {integrity: sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + dependencies: + '@babel/core': 7.26.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 9.14.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + dev: false - w3c-hr-time@1.0.2: - resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} - deprecated: Use your platform's native performance.now() and performance.timeOrigin. + /@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@9.14.0): + resolution: {integrity: sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + dependencies: + '@babel/core': 7.26.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 9.14.0 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 - w3c-xmlserializer@2.0.0: - resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} - engines: {node: '>=10'} + /@babel/generator@7.26.2: + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 - w3c-xmlserializer@3.0.0: - resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} - engines: {node: '>=12'} + /@babel/helper-annotate-as-pure@7.25.9: + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.0 - walk-sync@0.2.7: - resolution: {integrity: sha512-OH8GdRMowEFr0XSHQeX5fGweO6zSVHo7bG/0yJQx6LAj9Oukz0C8heI3/FYectT66gY0IPGe89kOvU410/UNpg==} + /@babel/helper-builder-binary-assignment-operator-visitor@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color - walk-sync@0.3.4: - resolution: {integrity: sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig==} + /@babel/helper-compilation-targets@7.25.9: + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 - walk-sync@1.1.4: - resolution: {integrity: sha512-nowc9thB/Jg0KW4TgxoRjLLYRPvl3DB/98S89r4ZcJqq2B0alNcKDh6pzLkBSkPMzRSMsJghJHQi79qw0YWEkA==} + /@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9(supports-color@8.1.1) + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - walk-sync@2.2.0: - resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} - engines: {node: 8.* || >= 10.*} + /@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9(supports-color@8.1.1) + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true - walk-sync@3.0.0: - resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} - engines: {node: 10.* || >= 12.*} + /@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.1.1 + semver: 6.3.1 - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + /@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0): + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.3.7(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color - watch-detector@0.1.0: - resolution: {integrity: sha512-vfzMMfpjQc88xjETwl2HuE6PjEuxCBeyC4bQmqrHrofdfYWi/4mEJklYbNgSzpqM9PxubsiPIrE5SZ1FDyiQ2w==} - engines: {node: '>= 4'} + /@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.3.7(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true - watch-detector@1.0.2: - resolution: {integrity: sha512-MrJK9z7kD5Gl3jHBnnBVHvr1saVGAfmkyyrvuNzV/oe0Gr1nwZTy5VSA0Gw2j2Or0Mu8HcjUa44qlBvC2Ofnpg==} - engines: {node: '>= 8'} + /@babel/helper-member-expression-to-functions@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color - watcher@2.3.1: - resolution: {integrity: sha512-d3yl+ey35h05r5EFP0TafE2jsmQUJ9cc2aernRVyAkZiu0J3+3TbNugNcqdUJDoWOfL2p+bNsN427stsBC/HnA==} + /@babel/helper-module-imports@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color - watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} + /@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + /@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + /@babel/helper-optimise-call-expression@7.25.9: + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.0 - webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} + /@babel/helper-plugin-utils@7.25.9: + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} - webidl-conversions@6.1.0: - resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} - engines: {node: '>=10.4'} + /@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + /@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} + /@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9(supports-color@8.1.1) + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - webpack@5.77.0: - resolution: {integrity: sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==} - engines: {node: '>=10.13.0'} - hasBin: true + /@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.25.9(supports-color@8.1.1) + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} + /@babel/helper-simple-access@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} + /@babel/helper-skip-transparent-expression-wrappers@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} - whatwg-encoding@1.0.5: - resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} - whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} + /@babel/helper-validator-option@7.25.9: + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} - whatwg-fetch@3.6.2: - resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} + /@babel/helper-wrap-function@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color - whatwg-mimetype@2.3.0: - resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + /@babel/helpers@7.26.0: + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 - whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} + /@babel/parser@7.26.2: + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.0 - whatwg-url@10.0.0: - resolution: {integrity: sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==} - engines: {node: '>=12'} + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - whatwg-url@11.0.0: - resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} - engines: {node: '>=12'} + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + /@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - whatwg-url@8.7.0: - resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} - engines: {node: '>=10'} + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - which-typed-array@1.1.9: - resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} - engines: {node: '>= 0.4'} + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - which@3.0.1: - resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0): + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + /@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - widest-line@3.1.0: - resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} - engines: {node: '>=8'} + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.26.0): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 - wordwrap@0.0.3: - resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} - engines: {node: '>=0.4.0'} + /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.26.0): + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + /@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - workerpool@3.1.2: - resolution: {integrity: sha512-WJFA0dGqIK7qj7xPTqciWBH5DlJQzoPjsANvc3Y4hNB0SScT+Emjvt0jPPkDBUjBNngX1q9hHgt1Gfwytu6pug==} + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.26.0): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true - workerpool@6.2.1: - resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + /@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - workerpool@6.4.0: - resolution: {integrity: sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A==} + /@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + /@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + dev: true - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + /@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - write-file-atomic@5.0.1: - resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - write-yaml-file@5.0.0: - resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==} - engines: {node: '>=16.14'} + /@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} + /@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - engines: {node: '>=10.0.0'} + /@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} + /@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - xdg-basedir@4.0.0: - resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} - engines: {node: '>=8'} + /@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - xml-name-validator@3.0.0: - resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + /@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} + /@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + /@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} + /@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + /@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + /@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color - yam@1.0.0: - resolution: {integrity: sha512-Hv9xxHtsJ9228wNhk03xnlDReUuWVvHwM4rIbjdAXYvHLs17xjuyF50N6XXFMN6N0omBaqgOok/MCK3At9fTAg==} - engines: {node: ^4.5 || 6.* || >= 7.*} + /@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/traverse': 7.25.9(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true - yargs-parser@20.2.4: - resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} - engines: {node: '>=10'} + /@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + /@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} + /@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + /@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - yargs@17.7.1: - resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} - engines: {node: '>=12'} + /@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + /@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + /@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - yui@3.18.1: - resolution: {integrity: sha512-M4/mHnq5uGvpwKEpRBh3SclL70cpDEus9LNGnrK5ZBzp4HOoueY7EkXfgtRBd+9VOQHWlFukXL2udHE53N4Wqw==} - engines: {node: '>=0.8.0'} + /@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - yuidocjs@0.10.2: - resolution: {integrity: sha512-g0ZrXsaCmQL9zsvkgD+RxWDsMNkHne5tK72iWYodro9JQlfKxePcV1dwbGhKMy/fl1XCIW3R3erZudohU+PcEw==} - engines: {node: '>=0.10.0'} - hasBin: true + /@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - zlib@1.0.5: - resolution: {integrity: sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==} - engines: {node: '>=0.2.0'} + /@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color -snapshots: + /@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - '@ampproject/remapping@2.2.0': + /@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.17 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - '@babel/cli@7.21.0(@babel/core@7.21.4)': + /@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@jridgewell/trace-mapping': 0.3.17 - commander: 4.1.1 - convert-source-map: 1.9.0 - fs-readdir-recursive: 1.1.0 - glob: 7.2.3 - make-dir: 2.1.0 - slash: 2.0.0 - optionalDependencies: - '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 - chokidar: 3.5.3 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - '@babel/cli@7.24.5(@babel/core@7.24.5)': + /@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@jridgewell/trace-mapping': 0.3.25 - commander: 4.1.1 - convert-source-map: 2.0.0 - fs-readdir-recursive: 1.1.0 - glob: 7.2.3 - make-dir: 2.1.0 - slash: 2.0.0 - optionalDependencies: - '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 - chokidar: 3.5.3 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/code-frame@7.18.6': + /@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/highlight': 7.18.6 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/code-frame@7.21.4': + /@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/highlight': 7.18.6 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/code-frame@7.24.2': + /@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/highlight': 7.24.5 - picocolors: 1.0.0 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/compat-data@7.21.4': {} + /@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - '@babel/compat-data@7.24.4': {} + /@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - '@babel/core@7.21.4': + /@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.4 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) - '@babel/helper-module-transforms': 7.21.2 - '@babel/helpers': 7.21.0 - '@babel/parser': 7.21.4 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.4 - '@babel/types': 7.21.4 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/core@7.24.5': + /@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helpers': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 - convert-source-map: 2.0.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9(supports-color@8.1.1) transitivePeerDependencies: - supports-color + dev: true - '@babel/core@7.24.5(supports-color@8.1.1)': + /@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5(supports-color@8.1.1)) - '@babel/helpers': 7.24.5(supports-color@8.1.1) - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5(supports-color@8.1.1) - '@babel/types': 7.24.5 - convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.21.3(@babel/core@7.21.4)(eslint@8.37.0)': + /@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.37.0 - eslint-visitor-keys: 2.1.0 - semver: 6.3.0 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - '@babel/generator@7.21.4': + /@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.21.4 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.17 - jsesc: 2.5.2 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - '@babel/generator@7.24.5': + /@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-annotate-as-pure@7.18.6': + /@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-annotate-as-pure@7.22.5': + /@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15': + /@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-compilation-targets@7.21.4(@babel/core@7.21.4)': + /@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.21.4 - '@babel/core': 7.21.4 - '@babel/helper-validator-option': 7.21.0 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-compilation-targets@7.23.6': + /@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.24.4 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 - lru-cache: 5.1.1 - semver: 6.3.1 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/helper-create-class-features-plugin@7.21.0(@babel/core@7.21.4)': + /@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-member-expression-to-functions': 7.21.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.20.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 - '@babel/helper-split-export-declaration': 7.18.6 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.21.4)': + /@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.21.4) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - semver: 6.3.1 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - semver: 6.3.1 + /@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-create-regexp-features-plugin@7.21.0(@babel/core@7.24.5)': + /@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.21.4)': + /@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 - semver: 6.3.1 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.5)': + /@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 - semver: 6.3.1 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.21.4)': + /@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.1 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.5)': + /@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color + dev: true - '@babel/helper-environment-visitor@7.18.9': {} - - '@babel/helper-environment-visitor@7.22.20': {} - - '@babel/helper-function-name@7.21.0': + /@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color - '@babel/helper-function-name@7.23.0': + /@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-hoist-variables@7.18.6': + /@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.21.4 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-hoist-variables@7.22.5': + /@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 - '@babel/helper-member-expression-to-functions@7.21.0': + /@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-member-expression-to-functions@7.24.5': + /@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-module-imports@7.18.6': + /@babel/plugin-transform-runtime@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.21.4 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@babel/helper-module-imports@7.21.4': + /@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-module-imports@7.24.3': + /@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - '@babel/helper-module-transforms@7.21.2': + /@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.21.4 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.4 - '@babel/types': 7.21.4 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) transitivePeerDependencies: - supports-color + dev: true - '@babel/helper-module-transforms@7.24.5(@babel/core@7.21.4)': + /@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5(supports-color@8.1.1))': + /@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5(supports-color@8.1.1) - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5)': + /@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-optimise-call-expression@7.18.6': + /@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - '@babel/helper-optimise-call-expression@7.22.5': + /@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-plugin-utils@7.20.2': {} - - '@babel/helper-plugin-utils@7.24.5': {} + /@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.21.4)': + /@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.5)': + /@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0): + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.24.5 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-replace-supers@7.20.7': + /@babel/preset-env@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.21.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.4 - '@babel/types': 7.21.4 + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.24.1(@babel/core@7.21.4)': + /@babel/preset-env@7.26.0(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0)(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0)(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0)(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0)(supports-color@8.1.1) + core-js-compat: 3.39.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-replace-supers@7.24.1(@babel/core@7.24.5)': + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.0 + esutils: 2.0.3 - '@babel/helper-simple-access@7.20.2': + /@babel/preset-typescript@7.26.0(@babel/core@7.26.0): + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/types': 7.21.4 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + dev: true - '@babel/helper-simple-access@7.24.5': + /@babel/runtime@7.12.18: + resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==} dependencies: - '@babel/types': 7.24.5 + regenerator-runtime: 0.13.11 - '@babel/helper-skip-transparent-expression-wrappers@7.20.0': + /@babel/runtime@7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.5 + regenerator-runtime: 0.14.1 - '@babel/helper-skip-transparent-expression-wrappers@7.22.5': + /@babel/template@7.25.9: + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.5 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 - '@babel/helper-split-export-declaration@7.18.6': + /@babel/traverse@7.25.9(supports-color@8.1.1): + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.21.4 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color - '@babel/helper-split-export-declaration@7.24.5': + /@babel/types@7.26.0: + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.5 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - '@babel/helper-string-parser@7.19.4': {} - - '@babel/helper-string-parser@7.24.1': {} - - '@babel/helper-validator-identifier@7.19.1': {} - - '@babel/helper-validator-identifier@7.24.5': {} + /@cnakazawa/watch@1.0.4: + resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} + engines: {node: '>=0.1.95'} + hasBin: true + dependencies: + exec-sh: 0.3.6 + minimist: 1.2.8 - '@babel/helper-validator-option@7.21.0': {} + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: true + optional: true - '@babel/helper-validator-option@7.23.5': {} + /@ember-data/rfc395-data@0.0.4: + resolution: {integrity: sha512-tGRdvgC9/QMQSuSuJV45xoyhI0Pzjm7A9o/MVVA3HakXIImJbbzx/k/6dO9CUEQXIyS2y0fW6C1XaYOG7rY0FQ==} - '@babel/helper-wrap-function@7.24.5': - dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 + /@ember/edition-utils@1.2.0: + resolution: {integrity: sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==} - '@babel/helpers@7.21.0': + /@ember/optional-features@2.2.0: + resolution: {integrity: sha512-a1OQ+w9vDvMXd9BNA9r779yr8MAPguGaMGbIeTMPWACeWBdD6bACBB5iKE3gNyrJAYKMq2wab6BKmRFS3Qw3hw==} + engines: {node: 10.* || 12.* || >= 14} dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.4 - '@babel/types': 7.21.4 + chalk: 4.1.2 + ember-cli-version-checker: 5.1.2 + glob: 7.2.3 + inquirer: 7.3.3 + mkdirp: 1.0.4 + silent-error: 1.1.1 transitivePeerDependencies: - supports-color - '@babel/helpers@7.24.5': + /@ember/string@3.1.1(@babel/core@7.26.0): + resolution: {integrity: sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) transitivePeerDependencies: + - '@babel/core' - supports-color - '@babel/helpers@7.24.5(supports-color@8.1.1)': + /@ember/string@4.0.0: + resolution: {integrity: sha512-IMVyVE72twuAMSYcHzWSgtgYTtzlHlKSGW8vEbztnnmkU6uo7kVHmiqSN9R4RkBhzvh0VD4G76Eph+55t3iNIA==} + dev: true + + /@ember/test-helpers@3.3.0(patch_hash=gppmtiox6pymwamrfimkbxfrsm)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0): + resolution: {integrity: sha512-HEI28wtjnQuEj9+DstHUEEKPtqPAEVN9AAVr4EifVCd3DyEDy0m6hFT4qbap1WxAIktLja2QXGJg50lVWzZc5g==} + engines: {node: 16.* || >= 18} + peerDependencies: + ember-source: '*' dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5(supports-color@8.1.1) - '@babel/types': 7.24.5 + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@simple-dom/interface': 1.4.0 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + dom-element-descriptors: 0.5.1 + ember-auto-import: 2.10.0(@glint/template@1.5.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-htmlbars: 6.3.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + webpack: 5.94.0 transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - '@swc/core' + - esbuild - supports-color + - uglify-js + - webpack-cli + dev: true + patched: true - '@babel/highlight@7.18.6': - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - - '@babel/highlight@7.24.5': - dependencies: - '@babel/helper-validator-identifier': 7.24.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.0 - - '@babel/parser@7.21.3': + /@ember/test-helpers@4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0): + resolution: {integrity: sha512-1mbOVyVEcLxYOGzBaeeaQkCrL1o9Av86QaHk/1RvrVBW24I6YUj1ILLEi2qLZT5PzcCy0TdfadHT3hKJwJ0GcQ==} + peerDependencies: + ember-source: '*' dependencies: - '@babel/types': 7.24.5 + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/addon-shim': 1.9.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@simple-dom/interface': 1.4.0 + decorator-transforms: 2.3.0(@babel/core@7.26.0) + dom-element-descriptors: 0.5.1 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + webpack: 5.94.0 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + patched: true - '@babel/parser@7.21.4': + /@ember/test-waiters@3.1.0(@babel/core@7.26.0): + resolution: {integrity: sha512-bb9h95ktG2wKY9+ja1sdsFBdOms2lB19VWs8wmNpzgHv1NCetonBoV5jHBV4DHt0uS1tg9z66cZqhUVlYs96KQ==} + engines: {node: 10.* || 12.* || >= 14.*} dependencies: - '@babel/types': 7.21.4 + calculate-cache-key-for-tree: 2.0.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-version-checker: 5.1.2 + semver: 7.6.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color - '@babel/parser@7.24.5': + /@embroider/addon-dev@4.3.1(rollup@4.25.0): + resolution: {integrity: sha512-CNZ4Y69PPIZAAGGoERjvDcrwOwWTuUmnRYu+XnmqKk0opdlu/PTssO9YWyxp8AnvGd2l7iLCjEn5mpLFvifstA==} + engines: {node: 12.* || 14.* || >= 16} + hasBin: true dependencies: - '@babel/types': 7.24.5 + '@embroider/core': 3.4.20 + '@rollup/pluginutils': 4.2.1 + content-tag: 2.0.3 + fs-extra: 10.1.0 + minimatch: 3.1.2 + rollup-plugin-copy-assets: 2.0.3(rollup@4.25.0) + rollup-plugin-delete: 2.1.0(rollup@4.25.0) + walk-sync: 3.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas + - rollup + - supports-color + - utf-8-validate + dev: false - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.5)': + /@embroider/addon-shim@1.9.0: + resolution: {integrity: sha512-fMzayl/licUL8VRAy4qXROKcYvHwUbV8aTh4m97L5/MRuVpxbcAy92DGGTqx5OBKCSQN3gMg+sUKeE6AviefpQ==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + broccoli-funnel: 3.0.8 + common-ancestor-path: 1.0.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.4)': + /@embroider/babel-loader-9@3.1.1(@embroider/core@3.4.19)(supports-color@8.1.1)(webpack@5.94.0): + resolution: {integrity: sha512-8mIDRXvwntYIQc2JFVvGXEppHUJRhw+6aEzHtbCZDr4oOKw55IyY+RHzas3JILRq64owLA+Ox0yu6nkwL1ApRQ==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@embroider/core': ^3.4.0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@embroider/core': 3.4.19 + babel-loader: 9.2.1(@babel/core@7.26.0)(webpack@5.94.0) + transitivePeerDependencies: + - supports-color + - webpack + dev: true - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@embroider/compat@3.7.0(@embroider/core@3.4.19): + resolution: {integrity: sha512-hv9BNWB278NgxGkpLaKT6VSaGckTX17EiddQpNGlqFEPw4jNuqpEeUGUgFBrSUBsO64wOGrY0U8pbRJsvGGE+Q==} + engines: {node: 12.* || 14.* || >= 16} + hasBin: true + peerDependencies: + '@embroider/core': ^3.4.19 + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.26.0 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-runtime': 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.26.0 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@embroider/core': 3.4.19 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@types/babel__code-frame': 7.0.6 + '@types/yargs': 17.0.33 + assert-never: 1.3.0 + babel-import-util: 2.1.1 + babel-plugin-ember-template-compilation: 2.3.0 + babel-plugin-syntax-dynamic-import: 6.18.0 + babylon: 6.18.0 + bind-decorator: 1.0.11 + broccoli: 3.5.2 + broccoli-concat: 4.2.5 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + chalk: 4.1.2 + debug: 4.3.7(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + fast-sourcemap-concat: 2.1.1 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + pkg-up: 3.1.0 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + semver: 7.6.3 + symlink-or-copy: 1.3.1 + tree-sync: 2.1.0 + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: true - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.4) + /@embroider/compat@3.7.1-unstable.4070ba7(@embroider/core@3.4.20-unstable.4070ba7)(@glimmer/component@1.1.2): + resolution: {integrity: sha512-pihMXG15GKIv08RPB1rx0JV9hBJF2gWfwZJYTS6izOvF0TyDqqLvm+jkMFB9u5oHiURZx3L8xtyRa9oVbh0+HQ==} + engines: {node: 12.* || 14.* || >= 16} + hasBin: true + peerDependencies: + '@embroider/core': ^3.4.20-unstable.4070ba7 + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.26.0 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-runtime': 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.26.0 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@embroider/core': 3.4.20-unstable.4070ba7 + '@embroider/macros': 1.16.10-unstable.4070ba7(@babel/core@7.26.0) + '@types/babel__code-frame': 7.0.6 + '@types/yargs': 17.0.33 + assert-never: 1.3.0 + babel-import-util: 2.1.1 + babel-plugin-debug-macros: 1.0.2(@babel/core@7.26.0) + babel-plugin-ember-template-compilation: 2.3.0 + babel-plugin-syntax-dynamic-import: 6.18.0 + babylon: 6.18.0 + bind-decorator: 1.0.11 + broccoli: 3.5.2 + broccoli-concat: 4.2.5 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + chalk: 4.1.2 + debug: 4.3.7(supports-color@8.1.1) + ember-source: 6.1.0-beta.1(@glimmer/component@1.1.2) + fast-sourcemap-concat: 2.1.1 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + pkg-up: 3.1.0 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + resolve.exports: 2.0.2 + semver: 7.6.3 + symlink-or-copy: 1.3.1 + tree-sync: 2.1.0 + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@glimmer/component' + - '@glint/template' + - '@swc/core' + - bufferutil + - canvas + - esbuild + - rsvp + - supports-color + - uglify-js + - utf-8-validate + - webpack-cli + dev: true - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) + /@embroider/config-meta-loader@0.0.1-unstable.4070ba7: + resolution: {integrity: sha512-fT/gZPSPzhciwDnRnG/N7oLFM4v07MnDAzmvA3VpDylxWGSFbnMIKcVt5GI8uykTcXe5NLJd+x2iEGECLjoFcg==} + engines: {node: 12.* || 14.* || >= 16} + dev: true - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.5)': + /@embroider/core@3.4.19: + resolution: {integrity: sha512-nnjQzXa+LkbqcSl7+a5sX6UKzeyHaiKrYCi/Wg5EG5OzyukiFmX2ZNI44fJ/U69htIphCZXAvLsMsEsUPm94ZA==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + assert-never: 1.3.0 + babel-plugin-ember-template-compilation: 2.3.0 + broccoli-node-api: 1.7.0 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + debug: 4.3.7(supports-color@8.1.1) + fast-sourcemap-concat: 2.1.1 + filesize: 10.1.6 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + js-string-escape: 1.0.1 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + semver: 7.6.3 + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: true - '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.4)': + /@embroider/core@3.4.20: + resolution: {integrity: sha512-bKy/uIrcoUyJXcBud2d8E7J8R8NRMz0T3xaKbEUx4nxc4/BbBsU+y7wSyzzfggykyJTK/95PwNIxls8gqnyYYA==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.21.4) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.4) + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@embroider/macros': 1.16.10(@babel/core@7.26.0) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + assert-never: 1.3.0 + babel-plugin-ember-template-compilation: 2.3.0 + broccoli-node-api: 1.7.0 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + debug: 4.3.7(supports-color@8.1.1) + fast-sourcemap-concat: 2.1.1 + filesize: 10.1.6 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + js-string-escape: 1.0.1 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + semver: 7.6.3 + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: false - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.4)': + /@embroider/core@3.4.20-unstable.4070ba7: + resolution: {integrity: sha512-v2bEIGZpp/OmeCRJVc+4tKf6XzWvoeUu9OkjC5+6kP8BTjo1hTMFjhC2Q06vy0BsBKZIuwIfa1CrM4rU810P3A==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + '@embroider/macros': 1.16.10-unstable.4070ba7(@babel/core@7.26.0) + '@embroider/reverse-exports': 0.1.1-unstable.4070ba7 + '@embroider/shared-internals': 2.8.2-unstable.4070ba7 + assert-never: 1.3.0 + babel-plugin-ember-template-compilation: 2.3.0 + broccoli-node-api: 1.7.0 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + debug: 4.3.7(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + fast-sourcemap-concat: 2.1.1 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + js-string-escape: 1.0.1 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + resolve.exports: 2.0.2 + semver: 7.6.3 + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas - supports-color + - utf-8-validate + dev: true - '@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.4)': + /@embroider/hbs-loader@3.0.3(@embroider/core@3.4.19)(webpack@5.94.0): + resolution: {integrity: sha512-sI2K3/III1WGGxS+aIf8uW5tgcNiE7APNhThn2ZTwqU47fK20Uz8TJZhst0GfNZFsCsmuQMRUikRJvQU8naSWA==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@embroider/core': ^3.4.0 + webpack: 5.94.0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.4) + '@embroider/core': 3.4.19 + webpack: 5.94.0 + dev: true - '@babel/plugin-proposal-decorators@7.21.0(@babel/core@7.21.4)': + /@embroider/macros@1.16.10(@babel/core@7.26.0): + resolution: {integrity: sha512-G0vCsKgNCX0PMmuVNsTLG7IYXz8VkekQMK4Kcllzqpwb7ivFRDwVx2bD4QSvZ9LCTd4eWQ654RsCqVbW5aviww==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': 1.5.0 + peerDependenciesMeta: + '@glint/template': + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.20.7 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/plugin-syntax-decorators': 7.21.0(@babel/core@7.21.4) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + assert-never: 1.3.0 + babel-import-util: 2.1.1 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + find-up: 5.0.0 + lodash: 4.17.21 + resolve: 1.22.8 + semver: 7.6.3 transitivePeerDependencies: + - '@babel/core' - supports-color - '@babel/plugin-proposal-decorators@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-decorators': 7.24.1(@babel/core@7.24.5) - - '@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.4) - - '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.4) - - '@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.4) - - '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.4) - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.4)': + /@embroider/macros@1.16.10-unstable.4070ba7(@babel/core@7.26.0): + resolution: {integrity: sha512-Gww0noS0CdMyV+bBBcwimIQYL6+S09RzAMaofgY8o4K3PU5YjQB/Bpr4ueuIA18pTgGcWFiR9qY2jVZSoP7SFA==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': 1.5.0 + peerDependenciesMeta: + '@glint/template': + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.4) + '@embroider/shared-internals': 2.8.2-unstable.4070ba7 + assert-never: 1.3.0 + babel-import-util: 2.1.1 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + find-up: 5.0.0 + lodash: 4.17.21 + resolve: 1.22.8 + semver: 7.6.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.4)': + /@embroider/macros@1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0): + resolution: {integrity: sha512-AUrmHQdixczIU3ouv/+HzWxwYVsw/NwssZxAQnXfBDJ3d3/CRtAvGRu3JhY6OT3AAPFwfa2WT66tB5jeAa7r5g==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': 1.5.0 + peerDependenciesMeta: + '@glint/template': + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.4) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + '@glint/template': 1.5.0 + assert-never: 1.3.0 + babel-import-util: 2.1.1 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + find-up: 5.0.0 + lodash: 4.17.21 + resolve: 1.22.8 + semver: 7.6.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color - '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.4)': + /@embroider/reverse-exports@0.1.1-unstable.4070ba7: + resolution: {integrity: sha512-/OFduk0+GODIiDcoS4GLxqarcAm7J5uHKf7QfBJNoMSUCVMiydVPs78WqHunbgh+TlGEv1EAqLIWRrY+Wu6LGQ==} dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.21.4) + resolve.exports: 2.0.2 + dev: true - '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.4)': + /@embroider/shared-internals@2.8.1(supports-color@8.1.1): + resolution: {integrity: sha512-zi0CENFD1e0DH7c9M/rNKJnFnt2c3+736J3lguBddZdmaIV6Cb8l3HQSkskSW5O4ady+SavemLKO3hCjQQJBIw==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.4) + babel-import-util: 2.1.1 + debug: 4.3.7(supports-color@8.1.1) + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + minimatch: 3.1.2 + pkg-entry-points: 1.1.1 + resolve-package-path: 4.0.3 + semver: 7.6.3 + typescript-memoize: 1.1.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.4)': + /@embroider/shared-internals@2.8.2-unstable.4070ba7: + resolution: {integrity: sha512-RybzhYZvYYAXnXbPakO0itiLzfbc5MlhZx5QgHc/r72FElemXySr3ydflKcPW0/8syKWL3AbqUEA64kdjD8Hug==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.4) + babel-import-util: 2.1.1 + debug: 4.3.7(supports-color@8.1.1) + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + minimatch: 3.1.2 + pkg-entry-points: 1.1.1 + resolve-package-path: 4.0.3 + resolve.exports: 2.0.2 + semver: 7.6.3 + typescript-memoize: 1.1.1 + transitivePeerDependencies: + - supports-color + dev: true - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.4)': + /@embroider/test-setup@4.0.1-unstable.4070ba7(@embroider/compat@3.7.1-unstable.4070ba7)(@embroider/core@3.4.20-unstable.4070ba7): + resolution: {integrity: sha512-Bgp2PyXTXm3iOlM/0YRtC8xq3v1AWb2EBmzS5hja+QCzp65NJ4gC9CSMM5Ry0I6SQpiKaYCVJfrKoGKP0xkp/Q==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@embroider/compat': ^3.7.1-unstable.4070ba7 + '@embroider/core': ^3.4.20-unstable.4070ba7 + '@embroider/webpack': ^4.0.9-unstable.4070ba7 + peerDependenciesMeta: + '@embroider/compat': + optional: true + '@embroider/core': + optional: true + '@embroider/webpack': + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 + '@embroider/compat': 3.7.1-unstable.4070ba7(@embroider/core@3.4.20-unstable.4070ba7)(@glimmer/component@1.1.2) + '@embroider/core': 3.4.20-unstable.4070ba7 + broccoli-plugin: 4.0.7 + lodash: 4.17.21 + resolve: 1.22.8 transitivePeerDependencies: - supports-color + dev: true - '@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.4) + /@embroider/vite@0.2.2-unstable.4070ba7(@embroider/core@3.4.20-unstable.4070ba7)(vite@5.4.11): + resolution: {integrity: sha512-41+Pu/7KbgOQOCwX0/E3YNvliBR9f1M/6LRexzxaoL4IrdmCi8MVhd1Ev7WZAp2aef/incJDxMC/Tv4LVGSh9g==} + peerDependencies: + '@embroider/core': ^3.4.20-unstable.4070ba7 + vite: ^5.2.0 + dependencies: + '@babel/core': 7.26.0 + '@embroider/core': 3.4.20-unstable.4070ba7 + '@embroider/macros': 1.16.10-unstable.4070ba7(@babel/core@7.26.0) + '@embroider/reverse-exports': 0.1.1-unstable.4070ba7 + '@rollup/pluginutils': 5.1.3(rollup@4.25.0) + assert-never: 1.3.0 + browserslist: 4.24.2 + browserslist-to-esbuild: 2.1.1(browserslist@4.24.2) + content-tag: 2.0.3 + debug: 4.3.7(supports-color@8.1.1) + esbuild: 0.17.19 + fast-glob: 3.3.2 + fs-extra: 10.1.0 + jsdom: 25.0.1(supports-color@8.1.1) + send: 0.18.0 + source-map-url: 0.4.1 + terser: 5.36.0 + vite: 5.4.11(@types/node@20.17.6) transitivePeerDependencies: + - '@glint/template' + - bufferutil + - canvas + - rollup - supports-color + - utf-8-validate + dev: true - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 + /@embroider/webpack@4.0.8(@embroider/core@3.4.19)(webpack@5.94.0): + resolution: {integrity: sha512-5i1v6+eH1gMHOqtaCzkFX6JPekmapN1+Clacxu+lxiv/piufuJV6bkugyPxIqqGBWjF8bOQA12ncM9BgpLae8A==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@embroider/core': ^3.4.19 + webpack: 5.94.0 + dependencies: + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0)(supports-color@8.1.1) + '@embroider/babel-loader-9': 3.1.1(@embroider/core@3.4.19)(supports-color@8.1.1)(webpack@5.94.0) + '@embroider/core': 3.4.19 + '@embroider/hbs-loader': 3.0.3(@embroider/core@3.4.19)(webpack@5.94.0) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + '@types/supports-color': 8.1.3 + assert-never: 1.3.0 + babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.94.0) + css-loader: 5.2.7(webpack@5.94.0) + csso: 4.2.0 + debug: 4.3.7(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + fs-extra: 9.1.0 + jsdom: 25.0.1(supports-color@8.1.1) + lodash: 4.17.21 + mini-css-extract-plugin: 2.9.2(webpack@5.94.0) + semver: 7.6.3 + source-map-url: 0.4.1 + style-loader: 2.0.0(webpack@5.94.0) + supports-color: 8.1.1 + terser: 5.36.0 + thread-loader: 3.0.4(webpack@5.94.0) + webpack: 5.94.0 + transitivePeerDependencies: + - bufferutil + - canvas + - utf-8-validate + dev: true - '@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + optional: true - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true - '@babel/plugin-syntax-decorators@7.21.0(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-decorators@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true - '@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true - '@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true - '@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-transform-arrow-functions@7.20.7(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true - '@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true - '@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true - '@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.21.4)': + /@eslint-community/eslint-utils@4.4.1(eslint@9.14.0): + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.21.4) + eslint: 9.14.0 + eslint-visitor-keys: 3.4.3 - '@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) + /@eslint-community/regexpp@4.12.1: + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.21.4)': + /@eslint/config-array@0.18.0: + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@eslint/object-schema': 2.1.4 + debug: 4.3.7(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@eslint/core@0.7.0: + resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.21.4)': + /@eslint/eslintrc@3.1.0: + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 + ajv: 6.12.6 + debug: 4.3.7(supports-color@8.1.1) + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.20.2 + /@eslint/js@9.14.0: + resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@babel/plugin-transform-block-scoping@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@eslint/object-schema@2.1.4: + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.5)': + /@eslint/plugin-kit@0.2.2: + resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + levn: 0.4.1 - '@babel/plugin-transform-class-static-block@7.24.4(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) + /@gar/promisify@1.1.3: + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + dev: true - '@babel/plugin-transform-classes@7.21.0(@babel/core@7.21.4)': + /@glimmer/compiler@0.92.4: + resolution: {integrity: sha512-xoR8F6fsgFqWbPbCfSgJuJ95vaLnXw0SgDCwyl/KMeeaSxpHwJbr8+BfiUl+7ko2A+HzrY5dPXXnGr4ZM+CUXw==} + engines: {node: '>= 16.0.0'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.21.4) - '@babel/helper-split-export-declaration': 7.24.5 - globals: 11.12.0 + '@glimmer/interfaces': 0.92.3 + '@glimmer/syntax': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/wire-format': 0.92.3 - '@babel/plugin-transform-classes@7.24.5(@babel/core@7.24.5)': + /@glimmer/component@1.1.2(@babel/core@7.26.0): + resolution: {integrity: sha512-XyAsEEa4kWOPy+gIdMjJ8XlzA3qrGH55ZDv6nA16ibalCR17k74BI0CztxuRds+Rm6CtbUVgheCVlcCULuqD7A==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) - '@babel/helper-split-export-declaration': 7.24.5 - globals: 11.12.0 + '@glimmer/di': 0.1.11 + '@glimmer/env': 0.1.7 + '@glimmer/util': 0.44.0 + broccoli-file-creator: 2.1.1 + broccoli-merge-trees: 4.2.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 5.3.0 + ember-cli-version-checker: 3.1.3 + ember-compatibility-helpers: 1.2.7(@babel/core@7.26.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color - '@babel/plugin-transform-computed-properties@7.20.7(@babel/core@7.21.4)': + /@glimmer/debug@0.92.4: + resolution: {integrity: sha512-waTBOdtp92MC3h/51mYbc4GRumO+Tsa5jbXLoewqALjE1S8bMu9qgkG7Cx635x3/XpjsD9xceMqagBvYhuI6tA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/vm': 0.92.3 - '@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.5)': + /@glimmer/destroyable@0.92.3: + resolution: {integrity: sha512-vQ+mzT9Vkf+JueY7L5XbZqK0WyEVTKv0HOLrw/zDw9F5Szn3F/8Ea/qbAClo3QK3oZeg+ulFTa/61rdjSFYHGA==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 - '@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@glimmer/di@0.1.11: + resolution: {integrity: sha512-moRwafNDwHTnTHzyyZC9D+mUSvYrs1Ak0tRPjjmCghdoHHIvMshVbEnwKb/1WmW5CUlKc2eL9rlAV32n3GiItg==} - '@babel/plugin-transform-destructuring@7.24.5(@babel/core@7.24.5)': + /@glimmer/encoder@0.92.3: + resolution: {integrity: sha512-DJ8DB33LxODjzCWRrxozHUaRqVyZj4p8jDLG42aCNmWo3smxrsjshcaVUwDmib24DW+dzR7kMc39ObMqT5zK0w==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/interfaces': 0.92.3 + '@glimmer/vm': 0.92.3 - '@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 + /@glimmer/env@0.1.7: + resolution: {integrity: sha512-JKF/a9I9jw6fGoz8kA7LEQslrwJ5jms5CXhu/aqkBWk+PmZ6pTl8mlb/eJ/5ujBGTiQzBhy5AIWF712iA+4/mw==} - '@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.21.4)': + /@glimmer/global-context@0.84.3: + resolution: {integrity: sha512-8Oy9Wg5IZxMEeAnVmzD2NkObf89BeHoFSzJgJROE/deutd3rxg83mvlOez4zBBGYwnTb+VGU2LYRpet92egJjA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + dev: true - '@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + /@glimmer/global-context@0.92.3: + resolution: {integrity: sha512-tvlK5pt6oSe3furJ1KsO9vG/KmF9S98HLrcR48XbfwXlkuxvUeS94cdQId4GCN5naeX4OC4xm6eEjZWdc2s+jw==} - '@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.21.4)': + /@glimmer/interfaces@0.84.3: + resolution: {integrity: sha512-dk32ykoNojt0mvEaIW6Vli5MGTbQo58uy3Epj7ahCgTHmWOKuw/0G83f2UmFprRwFx689YTXG38I/vbpltEjzg==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@simple-dom/interface': 1.4.0 - '@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.5)': + /@glimmer/interfaces@0.92.3: + resolution: {integrity: sha512-QwQeA01N+0h+TAi/J7iUnZtRuJy+093hNyagxDQBA6b1wCBw+q+al9+O6gmbWlkWE7EifzmNE1nnrgcecJBlJQ==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@simple-dom/interface': 1.4.0 - '@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.5)': + /@glimmer/manager@0.92.4: + resolution: {integrity: sha512-YMoarZT/+Ft2YSd+Wuu5McVsdP9y6jeAdVQGYFpno3NlL3TXYbl7ELtK7OGxFLjzQE01BdiUZZRvcY+a/s9+CQ==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) + '@glimmer/debug': 0.92.4 + '@glimmer/destroyable': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/reference': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/validator': 0.92.3 + '@glimmer/vm': 0.92.3 + + /@glimmer/node@0.92.4: + resolution: {integrity: sha512-a5GME7HQJZFJPQDdSetQI6jjKXXQi0Vdr3WuUrYwhienVTV5LG0uClbFE2yYWC7TX97YDHpRrNk1CC258rujkQ==} + dependencies: + '@glimmer/interfaces': 0.92.3 + '@glimmer/runtime': 0.92.4 + '@glimmer/util': 0.92.3 + '@simple-dom/document': 1.4.0 - '@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.4)': + /@glimmer/opcode-compiler@0.92.4: + resolution: {integrity: sha512-WnZSBwxNqW/PPD/zfxEg6BVR5tHwTm8fp76piix8BNCQ6CuzVn6HUJ5SlvBsOwyoRCmzt/pkKmBJn+I675KG4w==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/debug': 0.92.4 + '@glimmer/encoder': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/manager': 0.92.4 + '@glimmer/reference': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/wire-format': 0.92.3 - '@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.5)': + /@glimmer/owner@0.92.3: + resolution: {integrity: sha512-ZxmXIUCy6DOobhGDhA6kMpaXZS7HAucEgIl/qcjV9crlzGOO8H4j+n2x6nA/8zpuqvO0gYaBzqdNdu+7EgOEmw==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/util': 0.92.3 - '@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.5)': + /@glimmer/program@0.92.4: + resolution: {integrity: sha512-fkquujQ11lsGCWl/+XpZW2E7bjHj/g6/Ht292A7pSoANBD8Bz/gPYiPM+XuMwes9MApEsTEMjV4EXlyk2/Cirg==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) + '@glimmer/encoder': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.92.3 + '@glimmer/manager': 0.92.4 + '@glimmer/opcode-compiler': 0.92.4 + '@glimmer/util': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/wire-format': 0.92.3 - '@babel/plugin-transform-for-of@7.21.0(@babel/core@7.21.4)': + /@glimmer/reference@0.84.3: + resolution: {integrity: sha512-lV+p/aWPVC8vUjmlvYVU7WQJsLh319SdXuAWoX/SE3pq340BJlAJiEcAc6q52y9JNhT57gMwtjMX96W5Xcx/qw==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.84.3 + '@glimmer/interfaces': 0.84.3 + '@glimmer/util': 0.84.3 + '@glimmer/validator': 0.92.3 + dev: true - '@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.5)': + /@glimmer/reference@0.92.3: + resolution: {integrity: sha512-Ud4LE689mEXL6BJnJx0ZPt2dt/A540C+TAnBFXHpcAjROz5gT337RN+tgajwudEUqpufExhcPSMGzs1pvWYCJg==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/validator': 0.92.3 - '@babel/plugin-transform-function-name@7.18.9(@babel/core@7.21.4)': + /@glimmer/runtime@0.92.4: + resolution: {integrity: sha512-ISqM/8hVh+fY/gnLAAPKfts4CvnJBOyCYAXgGccIlzzQrSVLaz0NoRiWTLGj5B/3xyPbqLwYPDvlTsOjYtvPoA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - - '@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.5)': + '@glimmer/destroyable': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/manager': 0.92.4 + '@glimmer/owner': 0.92.3 + '@glimmer/program': 0.92.4 + '@glimmer/reference': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/validator': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/wire-format': 0.92.3 + + /@glimmer/syntax@0.84.3: + resolution: {integrity: sha512-ioVbTic6ZisLxqTgRBL2PCjYZTFIwobifCustrozRU2xGDiYvVIL0vt25h2c1ioDsX59UgVlDkIK4YTAQQSd2A==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.84.3 + '@glimmer/util': 0.84.3 + '@handlebars/parser': 2.0.0 + simple-html-tokenizer: 0.5.11 - '@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.5)': + /@glimmer/syntax@0.92.3: + resolution: {integrity: sha512-7wPKQmULyXCYf0KvbPmfrs/skPISH2QGR9atCnmDWnHyLv5SSZVLm1P0Ctrpta6+Ci3uGQb7hGk0IjsLEavcYQ==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/wire-format': 0.92.3 + '@handlebars/parser': 2.0.0 + simple-html-tokenizer: 0.5.11 - '@babel/plugin-transform-literals@7.18.9(@babel/core@7.21.4)': + /@glimmer/tracking@1.1.2: + resolution: {integrity: sha512-cyV32zsHh+CnftuRX84ALZpd2rpbDrhLhJnTXn9W//QpqdRZ5rdMsxSY9fOsj0CKEc706tmEU299oNnDc0d7tA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + '@glimmer/validator': 0.92.3 - '@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@glimmer/util@0.44.0: + resolution: {integrity: sha512-duAsm30uVK9jSysElCbLyU6QQYO2X9iLDLBIBUcCqck9qN1o3tK2qWiHbGK5d6g8E2AJ4H88UrfElkyaJlGrwg==} - '@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.5)': + /@glimmer/util@0.84.3: + resolution: {integrity: sha512-qFkh6s16ZSRuu2rfz3T4Wp0fylFj3HBsONGXQcrAdZjdUaIS6v3pNj6mecJ71qRgcym9Hbaq/7/fefIwECUiKw==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.84.3 + '@simple-dom/interface': 1.4.0 - '@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.21.4)': + /@glimmer/util@0.92.3: + resolution: {integrity: sha512-K1oH93gGU36slycxJ9CcFpUTsdOc4XQ6RuZFu5oRsxFYtEF5PSu7ik11h58fyeoaWOr1ebfkyAMawbeI2AJ5GA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.92.3 - '@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.5)': + /@glimmer/validator@0.92.3: + resolution: {integrity: sha512-HKrMYeW0YhiksSeKYqX2chUR/rz82j12DcY7p2dORQlTV3qlAfiE5zRTJH1KRA1X3ZMf7DI2/GOzkXwYp0o+3Q==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 - '@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.21.4)': + /@glimmer/vm-babel-plugins@0.92.3(@babel/core@7.26.0): + resolution: {integrity: sha512-VpkKsHc3oiq9ruiwT7sN4RuOIc5n10PCeWX7tYSNZ85S1bETcAFn0XbyNjI+G3uFshQGEK0T8Fn3+/8VTNIQIg==} + engines: {node: '>=16'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.21.2 - '@babel/helper-plugin-utils': 7.20.2 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.26.0) transitivePeerDependencies: - - supports-color + - '@babel/core' - '@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.24.5)': + /@glimmer/vm@0.92.3: + resolution: {integrity: sha512-DNMQz7nn2zRwKO1irVZ4alg1lH+VInwR3vkWVgobUs0yh7OoHVGXKMd5uxzIksqJEUw1XOX9Qgu/GYZB1PiH3w==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.21.2 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 - '@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.5)': + /@glimmer/wire-format@0.92.3: + resolution: {integrity: sha512-gFz81Q9+V7Xs0X8mSq6y8qacHm0dPaGJo2/Bfcsdow1hLOKNgTCLr4XeDBhRML8f6I6Gk9ugH4QDxyIOXOpC4w==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/interfaces': 0.92.3 + '@glimmer/util': 0.92.3 - '@babel/plugin-transform-modules-commonjs@7.21.2(@babel/core@7.21.4)': + /@glint/core@1.5.0(typescript@5.6.3): + resolution: {integrity: sha512-oo6ZDwX2S0Qqjai/CJH72LHg1U6rvzH1IyiFlWofaFiu/nSg04CDWZuJNPC3r47jz1+SaSI+mVMUaKJznzxzzQ==} + hasBin: true + peerDependencies: + typescript: '*' dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.21.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.20.2 + '@glimmer/syntax': 0.84.3 + escape-string-regexp: 4.0.0 + semver: 7.6.3 + silent-error: 1.1.1 + typescript: 5.6.3 + uuid: 8.3.2 + vscode-languageserver: 8.1.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + yargs: 17.7.2 transitivePeerDependencies: - supports-color + dev: true - '@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-simple-access': 7.24.5 - - '@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - - '@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - - '@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 - - '@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - - '@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 - - '@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - - '@babel/plugin-transform-new-target@7.18.6(@babel/core@7.21.4)': + /@glint/environment-ember-loose@1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0): + resolution: {integrity: sha512-QCP4pVupq8zGcBmMDcEq9XI5lfrnklwNOIuzdXb8OnbcY6qpuwz5Y6VOsA1WNGRcip/5wwOsmI6gsAEUTlbvPQ==} + peerDependencies: + '@glimmer/component': '*' + '@glint/template': 1.5.0 + '@types/ember__array': ^4.0.2 + '@types/ember__component': ^4.0.10 + '@types/ember__controller': ^4.0.2 + '@types/ember__object': ^4.0.4 + '@types/ember__routing': ^4.0.11 + ember-cli-htmlbars: ^6.3.0 + ember-modifier: ^3.2.7 || ^4.0.0 + peerDependenciesMeta: + '@types/ember__array': + optional: true + '@types/ember__component': + optional: true + '@types/ember__controller': + optional: true + '@types/ember__object': + optional: true + '@types/ember__routing': + optional: true + ember-cli-htmlbars: + optional: true + ember-modifier: + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@glimmer/component': 1.1.2(@babel/core@7.26.0) + '@glint/template': 1.5.0 + ember-cli-htmlbars: 6.3.0 + dev: true - '@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.5)': + /@glint/environment-ember-template-imports@1.5.0(@glint/environment-ember-loose@1.5.0)(@glint/template@1.5.0): + resolution: {integrity: sha512-SS+KNffLuNYcsT7iEmCr2jp2538E7KTMEAWY+KWNvUJ0ZMd6oe6xbIIF50+9BgCgGHWwj7oL/NdgCVkS3OqRdw==} + peerDependencies: + '@glint/environment-ember-loose': 1.5.0 + '@glint/template': 1.5.0 + '@types/ember__component': ^4.0.10 + '@types/ember__helper': ^4.0.1 + '@types/ember__modifier': ^4.0.3 + '@types/ember__routing': ^4.0.12 + peerDependenciesMeta: + '@types/ember__component': + optional: true + '@types/ember__helper': + optional: true + '@types/ember__modifier': + optional: true + '@types/ember__routing': + optional: true dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@glint/environment-ember-loose': 1.5.0(@glimmer/component@1.1.2)(@glint/template@1.5.0)(ember-cli-htmlbars@6.3.0) + '@glint/template': 1.5.0 + content-tag: 2.0.3 + dev: true - '@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) + /@glint/template@1.5.0: + resolution: {integrity: sha512-KyQUCWifxl8wDxo3SXzJcGKttHbIPgFBtqsoiu13Edx/o4CgGXr5rrM64jJR7Wvunn8sRM+Rq7Y0cHoB068Wuw==} - '@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) + /@gwhitney/detect-indent@7.0.1: + resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==} + engines: {node: '>=12.20'} - '@babel/plugin-transform-object-rest-spread@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) + /@handlebars/parser@2.0.0: + resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} - '@babel/plugin-transform-object-super@7.18.6(@babel/core@7.21.4)': + /@hono/node-server@1.13.7(hono@4.6.9): + resolution: {integrity: sha512-kTfUMsoloVKtRA2fLiGSd9qBddmru9KadNyhJCwgKBxTiNkaAJEwkVN9KV/rS4HtmmNRtUh6P+YpmjRMl0d9vQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.21.4) + hono: 4.6.9 - '@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) + /@humanfs/core@0.19.1: + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} - '@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.5)': + /@humanfs/node@0.16.6: + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 - '@babel/plugin-transform-optional-chaining@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} - '@babel/plugin-transform-parameters@7.21.3(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@humanwhocodes/retry@0.3.1: + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} - '@babel/plugin-transform-parameters@7.24.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@humanwhocodes/retry@0.4.1: + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} - '@babel/plugin-transform-parameters@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@inquirer/figures@1.0.8: + resolution: {integrity: sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==} + engines: {node: '>=18'} - '@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.5)': + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true - '@babel/plugin-transform-private-property-in-object@7.24.5(@babel/core@7.24.5)': + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 - '@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - '@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} - '@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.21.4)': + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - regenerator-transform: 0.15.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 - '@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - regenerator-transform: 0.15.2 + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.21.4)': + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 - '@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.5)': + /@lint-todo/utils@13.1.1: + resolution: {integrity: sha512-F5z53uvRIF4dYfFfJP3a2Cqg+4P1dgJchJsFnsZE0eZp0LK8X7g2J0CsJHRgns+skpXOlM7n5vFGwkWCWj8qJg==} + engines: {node: 12.* || >= 14} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@types/eslint': 8.56.12 + find-up: 5.0.0 + fs-extra: 9.1.0 + proper-lockfile: 4.1.2 + slash: 3.0.0 + tslib: 2.8.1 + upath: 2.0.1 + dev: true - '@babel/plugin-transform-runtime@7.21.4(@babel/core@7.21.4)': + /@microsoft/api-extractor-model@7.28.13: + resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-imports': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.4) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.4) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.4) - semver: 6.3.0 + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2 transitivePeerDependencies: - - supports-color + - '@types/node' + dev: false - '@babel/plugin-transform-runtime@7.24.3(@babel/core@7.24.5)': + /@microsoft/api-extractor@7.43.0: + resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} + hasBin: true dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.5 - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.5) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.5) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.5) - semver: 6.3.1 + '@microsoft/api-extractor-model': 7.28.13 + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2 + '@rushstack/rig-package': 0.5.2 + '@rushstack/terminal': 0.10.0 + '@rushstack/ts-command-line': 4.19.1 + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.8 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 transitivePeerDependencies: - - supports-color + - '@types/node' + dev: false - '@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.21.4)': + /@microsoft/tsdoc-config@0.16.2: + resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@microsoft/tsdoc': 0.14.2 + ajv: 6.12.6 + jju: 1.4.0 + resolve: 1.19.0 + dev: false - '@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@microsoft/tsdoc@0.14.2: + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + dev: false - '@babel/plugin-transform-spread@7.20.7(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + /@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3: + resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + requiresBuild: true + dev: false + optional: true - '@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.5)': + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + eslint-scope: 5.1.1 - '@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.21.4)': + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - '@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.5)': + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 - '@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.21.4)': + /@npmcli/fs@1.1.1: + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + '@gar/promisify': 1.1.3 + semver: 7.6.3 + dev: true - '@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.5)': + /@npmcli/move-file@1.1.2: + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + mkdirp: 1.0.4 + rimraf: 3.0.2 + dev: true - '@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true - '@babel/plugin-transform-typeof-symbol@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + /@pkgr/core@0.1.1: + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dev: true - '@babel/plugin-transform-typescript@7.21.3(@babel/core@7.21.4)': + /@pnpm/cli-meta@5.0.1: + resolution: {integrity: sha512-s7rVArn3s78w2ZDWC2/NzMaYBzq39QBmo1BQ4+qq1liX+ltSErDyAx3M/wvvJQgc+Ur3dZJYuc9t96roPnW3XQ==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.4) - transitivePeerDependencies: - - supports-color + '@pnpm/types': 9.1.0 + load-json-file: 6.2.0 - '@babel/plugin-transform-typescript@7.24.5(@babel/core@7.24.5)': + /@pnpm/cli-utils@2.0.9(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-mNujOPCopIi4r7D2HJ96hHKPEr/UPuZGruQvPVvjoc/pCP0l+y38xZAT72W2WhEM4Fo/zP8L+6g/zf88qUSbbg==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) + '@pnpm/cli-meta': 5.0.1 + '@pnpm/config': 18.4.0(@pnpm/logger@5.2.0) + '@pnpm/default-reporter': 12.2.3(@pnpm/logger@5.2.0) + '@pnpm/error': 5.0.1 + '@pnpm/logger': 5.2.0 + '@pnpm/manifest-utils': 5.0.1(@pnpm/logger@5.2.0) + '@pnpm/package-is-installable': 8.0.2(@pnpm/logger@5.2.0) + '@pnpm/read-project-manifest': 5.0.1 + '@pnpm/types': 9.1.0 + chalk: 4.1.2 + load-json-file: 6.2.0 - '@babel/plugin-transform-typescript@7.4.5(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.21.4) + /@pnpm/config.env-replace@1.1.0: + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} - '@babel/plugin-transform-typescript@7.5.5(@babel/core@7.21.4)': + /@pnpm/config@18.4.0(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-8B4Pw7cnMvO3kYUBZYYIjg6BcGhHwxEEkmBAcqAeF9NM6LmG6F0lFNsOf6XPfHZMx2vUTpZxaWo0FQo1uU2AAw==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.21.4) + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/constants': 7.1.0 + '@pnpm/error': 5.0.1 + '@pnpm/git-utils': 1.0.0 + '@pnpm/matcher': 5.0.0 + '@pnpm/npm-conf': 2.2.0 + '@pnpm/pnpmfile': 5.0.7(@pnpm/logger@5.2.0) + '@pnpm/read-project-manifest': 5.0.1 + '@pnpm/types': 9.1.0 + better-path-resolve: 1.0.0 + camelcase: 6.3.0 + camelcase-keys: 6.2.2 + can-write-to-dir: 1.1.1 + is-subdir: 1.2.0 + is-windows: 1.0.2 + normalize-registry-url: 2.0.0 + path-absolute: 1.0.1 + path-name: 1.0.0 + ramda: /@pnpm/ramda@0.28.1 + read-ini-file: 4.0.0 + realpath-missing: 1.1.0 + which: 3.0.1 + transitivePeerDependencies: + - '@pnpm/logger' - '@babel/plugin-transform-typescript@7.5.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) + /@pnpm/constants@7.1.0: + resolution: {integrity: sha512-PzpiPtGF+bIrmkNaHgOIfBZw669+rkUtt/5UFzHukiETwI4/+BTYz8FAr+m5Dfuns531Y+fYRFOpB0PdbAU0+w==} + engines: {node: '>=16.14'} - '@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 + /@pnpm/constants@7.1.1: + resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} + engines: {node: '>=16.14'} - '@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.5)': + /@pnpm/core-loggers@9.0.1(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-qP/kk6OeLSxqhvA4n6u4XB6evqD9h1w9p4qtdBOVbkZloCK7L9btkSmKNolBoQ3wrOz7WRFfjRekYUSKphMMCg==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 + '@pnpm/logger': 5.2.0 + '@pnpm/types': 9.1.0 - '@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.5)': + /@pnpm/dedupe.issues-renderer@1.0.0: + resolution: {integrity: sha512-vlo2t1ERLH3vsL1PtlCue6qfpWofN2Pt2bvGIPtN6Y4siCZVwjy9GU3yXJk1wS2+a7qj9plPiobebadJgV/VHw==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + '@pnpm/dedupe.types': 1.0.0 + archy: 1.0.0 + chalk: 4.1.2 - '@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.21.4)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.24.5 + /@pnpm/dedupe.types@1.0.0: + resolution: {integrity: sha512-WGZ5E7aMPwaM+WMFYszTCP3Sms/gE0nLgI37gFnNbaKgAh5R7GojSHCxLgXqjiz0Jwx+Qi9BmdDgN1cJs5XBsg==} + engines: {node: '>=16.14'} - '@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.5)': + /@pnpm/default-reporter@12.2.3(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-ALV6AQOcRPJ5bZlcCHDFQ4cEqH2B/2Luu0VYoAoofINgbhNDOKCrV6PkqLvnMQps98k1f7mtn4w/u4r99+qr7g==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + '@pnpm/config': 18.4.0(@pnpm/logger@5.2.0) + '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.2.0) + '@pnpm/dedupe.issues-renderer': 1.0.0 + '@pnpm/dedupe.types': 1.0.0 + '@pnpm/error': 5.0.1 + '@pnpm/logger': 5.2.0 + '@pnpm/render-peer-issues': 4.0.1 + '@pnpm/types': 9.1.0 + ansi-diff: 1.2.0 + boxen: 5.1.2 + chalk: 4.1.2 + normalize-path: 3.0.0 + pretty-bytes: 5.6.0 + pretty-ms: 7.0.1 + ramda: /@pnpm/ramda@0.28.1 + right-pad: 1.0.1 + rxjs: 7.8.1 + semver: 7.6.3 + stacktracey: 2.1.8 + string-length: 4.0.2 + strip-ansi: 6.0.1 - '@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.5)': + /@pnpm/error@5.0.1: + resolution: {integrity: sha512-JQSOeSEqrV6k6+kKgrlSJ7gddJRcjxtNCxSVJRIqwckkGSdSTNpXmKEdGgLlaDuEwElPAZUmLDGSqk5InJ5pMA==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 + '@pnpm/constants': 7.1.0 - '@babel/polyfill@7.12.1': + /@pnpm/error@5.0.3: + resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} + engines: {node: '>=16.14'} dependencies: - core-js: 2.6.12 - regenerator-runtime: 0.13.11 - - '@babel/preset-env@7.21.4(@babel/core@7.21.4)': - dependencies: - '@babel/compat-data': 7.21.4 - '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.4) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.21.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.4) - '@babel/plugin-transform-arrow-functions': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-transform-async-to-generator': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-transform-classes': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-transform-computed-properties': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-transform-destructuring': 7.21.3(@babel/core@7.21.4) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-for-of': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.21.4) - '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.21.4) - '@babel/plugin-transform-modules-systemjs': 7.20.11(@babel/core@7.21.4) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.21.4) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.4) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-regenerator': 7.20.5(@babel/core@7.21.4) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-spread': 7.20.7(@babel/core@7.21.4) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.21.4) - '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.21.4) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.4) - '@babel/preset-modules': 0.1.5(@babel/core@7.21.4) - '@babel/types': 7.21.4 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.4) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.4) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.4) - core-js-compat: 3.29.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - - '@babel/preset-env@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.5) - '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.5) - '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-class-static-block': 7.24.4(@babel/core@7.24.5) - '@babel/plugin-transform-classes': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-destructuring': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.5) - '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-object-rest-spread': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-private-property-in-object': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-typeof-symbol': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.5) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.5) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.5) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.5) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.5) - core-js-compat: 3.37.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@pnpm/constants': 7.1.1 - '@babel/preset-modules@0.1.5(@babel/core@7.21.4)': + /@pnpm/fetcher-base@14.0.1: + resolution: {integrity: sha512-DXPZ33CrmDQXnYzwvqyP7I0BF0MQELo4ah2JGpXhLhgOdzU+vj7zdKFo2x82L8anrK861IRi01V8o14oATq1vA==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.21.4) - '@babel/types': 7.24.5 - esutils: 2.0.3 + '@pnpm/resolver-base': 10.0.1 + '@pnpm/types': 9.1.0 + '@types/ssri': 7.1.5 - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.5)': + /@pnpm/find-workspace-dir@6.0.3: + resolution: {integrity: sha512-0iJnNkS4T8lJE4ldOhRERgER1o59iHA1nMlvpUI5lxNC9SUruH6peRUOlP4/rNcDg+UQ9u0rt5loYOnWKCojtw==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/types': 7.24.5 - esutils: 2.0.3 + '@pnpm/error': 5.0.3 + find-up: 5.0.0 - '@babel/preset-typescript@7.21.4(@babel/core@7.21.4)': + /@pnpm/find-workspace-packages@6.0.9(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-80t6m6w3EfOg5k88CR8Eya6aOJi2uXyYGFSv2Y+3DqGAWD2x6CFLM3kop2Zi1nL9THMYpYF3hLnBRbqcJ8rmRg==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.21.4) - '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.21.4) - '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.4) + '@pnpm/cli-utils': 2.0.9(@pnpm/logger@5.2.0) + '@pnpm/constants': 7.1.0 + '@pnpm/fs.find-packages': 2.0.1 + '@pnpm/types': 9.1.0 + '@pnpm/util.lex-comparator': 1.0.0 + read-yaml-file: 2.1.0 transitivePeerDependencies: - - supports-color + - '@pnpm/logger' - '@babel/preset-typescript@7.24.1(@babel/core@7.24.5)': + /@pnpm/fs.find-packages@2.0.1: + resolution: {integrity: sha512-QxG4YrnqnFdi9zmGxzUUH7YF6hgFqtPjDmiMlUvPbASSFRIr6mIT1rTynos2cbg0bRGXpLpp+0XtyOMdDGnBnQ==} + engines: {node: '>=16.14'} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) - - '@babel/regjsgen@0.8.0': {} + '@pnpm/read-project-manifest': 5.0.1 + '@pnpm/types': 9.1.0 + '@pnpm/util.lex-comparator': 1.0.0 + fast-glob: 3.3.2 + p-filter: 2.1.0 - '@babel/runtime@7.12.18': + /@pnpm/fs.hard-link-dir@2.0.1(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-ZsNyKG9YV9rZRhubj8bLxnysLg1LUwh0rdlVnp6ScIT9CtAA+C74kx2KK9E4TNofgb3qAbRqU4aKOaAoLmTSjg==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - regenerator-runtime: 0.13.11 + '@pnpm/logger': 5.2.0 - '@babel/runtime@7.21.0': + /@pnpm/git-utils@1.0.0: + resolution: {integrity: sha512-lUI+XrzOJN4zdPGOGnFUrmtXAXpXi8wD8OI0nWOZmlh+raqbLzC3VkXu1zgaduOK6YonOcnQW88O+ojav1rAdA==} + engines: {node: '>=16.14'} dependencies: - regenerator-runtime: 0.13.11 + execa: /safe-execa@0.1.2 - '@babel/runtime@7.24.5': + /@pnpm/graceful-fs@3.0.0: + resolution: {integrity: sha512-72kkqIL2sacOVr6Y6B6xDGjRC4QgTLeIGkw/5XYyeMgMeL9mDE0lonZEOL9JuLS0XPOXQoyDtRCSmUrzAA57LQ==} + engines: {node: '>=16.14'} dependencies: - regenerator-runtime: 0.14.1 + graceful-fs: 4.2.11 - '@babel/template@7.20.7': + /@pnpm/graceful-fs@3.2.0: + resolution: {integrity: sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==} + engines: {node: '>=16.14'} dependencies: - '@babel/code-frame': 7.21.4 - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + graceful-fs: 4.2.11 - '@babel/template@7.24.0': + /@pnpm/hooks.types@1.0.1: + resolution: {integrity: sha512-Zx2hzwxBKv1RmFzyu4pEVY7QeIGUb54smSSYt8GcJgByn+uMXgwJ7ydv9t2Koc90QTqk8J3P2J+RDrZVIQpVQw==} + engines: {node: '>=16.14'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 + '@pnpm/lockfile-types': 5.1.0 + '@pnpm/types': 9.1.0 - '@babel/traverse@7.21.3': + /@pnpm/lockfile-types@5.1.0: + resolution: {integrity: sha512-14eYp9iOdJ7SyOIVXomXhbVnc14DEhzMLS3eKqxYxi9LkANUfxx1/pwRiRY/lTiP9RFS+OkIcTm2QiLsmNEctw==} + engines: {node: '>=16.14'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@pnpm/types': 9.1.0 - '@babel/traverse@7.21.4': + /@pnpm/logger@5.2.0: + resolution: {integrity: sha512-dCdSs2wPCweMkRLdISAKBOKSWeq/9iS9aanWgjoUkFs06KN2o5XGFg53oCXg/KbZhF9AXS3vMHPwTebzCeAEsA==} + engines: {node: '>=18.12'} dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.4 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + bole: 5.0.17 + ndjson: 2.0.0 - '@babel/traverse@7.24.5': + /@pnpm/manifest-utils@5.0.1(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-vQUmd0NQNv1yWEeFA4pjuBCs4AqhaHW4bVpuaD19lHE5J9SCs7iNRDpjnxjTm/qgDgO/hqu/spuAXEbPxR8u0A==} + engines: {node: '>=16.14'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.4 - globals: 11.12.0 + '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.2.0) + '@pnpm/error': 5.0.1 + '@pnpm/types': 9.1.0 transitivePeerDependencies: - - supports-color + - '@pnpm/logger' - '@babel/traverse@7.24.5(supports-color@8.1.1)': + /@pnpm/matcher@5.0.0: + resolution: {integrity: sha512-uh+JBmW8XHGwz9x0K0Ok+TtMiu3ghEaqHHm7dqIubitBP8y9Y0LLP6D2fxWblogjpVzSlH3DpDR1Vicuhw9/cQ==} + engines: {node: '>=16.14'} dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.4(supports-color@8.1.1) - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + escape-string-regexp: 4.0.0 - '@babel/types@7.21.3': + /@pnpm/network.ca-file@1.0.2: + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 + graceful-fs: 4.2.10 - '@babel/types@7.21.4': + /@pnpm/npm-conf@2.2.0: + resolution: {integrity: sha512-roLI1ul/GwzwcfcVpZYPdrgW2W/drLriObl1h+yLF5syc8/5ULWw2ALbCHUWF+4YltIqA3xFSbG4IwyJz37e9g==} + engines: {node: '>=12'} dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 - '@babel/types@7.24.5': + /@pnpm/package-is-installable@8.0.2(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-eYuqNBjzYf5wXbD4Xm6ZupRPjYxn2sp6mtYL9+bMntx1+yoUlCJABrYcSvbTM7kheoHyHRf+gEQDFKdn5trQ6w==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.24.5 - to-fast-properties: 2.0.0 + '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.2.0) + '@pnpm/error': 5.0.1 + '@pnpm/logger': 5.2.0 + '@pnpm/types': 9.1.0 + detect-libc: 2.0.3 + execa: /safe-execa@0.1.2 + mem: 8.1.1 + semver: 7.6.3 - '@cnakazawa/watch@1.0.4': + /@pnpm/pnpmfile@5.0.7(@pnpm/logger@5.2.0): + resolution: {integrity: sha512-A8uwamvs9jhf3DYLuGHCngWW8WXEDgcm3nwOeRTWJOOgButgXueIRHcEZPiKgQwy6t116ntimNeW5H3/hjim6w==} + engines: {node: '>=16.14'} + peerDependencies: + '@pnpm/logger': ^5.0.0 dependencies: - exec-sh: 0.3.6 - minimist: 1.2.8 + '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.2.0) + '@pnpm/error': 5.0.1 + '@pnpm/hooks.types': 1.0.1 + '@pnpm/lockfile-types': 5.1.0 + '@pnpm/logger': 5.2.0 + '@pnpm/store-controller-types': 15.0.1 + '@pnpm/types': 9.1.0 + chalk: 4.1.2 + path-absolute: 1.0.1 - '@colors/colors@1.5.0': - optional: true + /@pnpm/ramda@0.28.1: + resolution: {integrity: sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==} - '@ember-data/adapter@file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2)': + /@pnpm/read-project-manifest@5.0.1: + resolution: {integrity: sha512-MDXuQpYFbabSXzAnqP7VIQqBx5Z1fzOhzB/3YmIXJ+tE7Wue//IR3itMSYlWeaFLo1G5PCJklM2zBdvggRw1nw==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + '@gwhitney/detect-indent': 7.0.1 + '@pnpm/error': 5.0.1 + '@pnpm/graceful-fs': 3.0.0 + '@pnpm/text.comments-parser': 2.0.0 + '@pnpm/types': 9.1.0 + '@pnpm/write-project-manifest': 5.0.1 + fast-deep-equal: 3.1.3 + is-windows: 1.0.2 + json5: 2.2.3 + parse-json: 5.2.0 + read-yaml-file: 2.1.0 + sort-keys: 4.2.0 + strip-bom: 4.0.0 - '@ember-data/adapter@file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': + /@pnpm/read-project-manifest@5.0.11: + resolution: {integrity: sha512-themRLiDt9Ud6Somlu0PJbeprBBQEhlI1xNG5bZIv26yfLsc1vYLd1TfgGViD1b8fP0jxAqsUrDM+WMaMKI+gw==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + '@gwhitney/detect-indent': 7.0.1 + '@pnpm/error': 5.0.3 + '@pnpm/graceful-fs': 3.2.0 + '@pnpm/text.comments-parser': 2.0.0 + '@pnpm/types': 9.4.2 + '@pnpm/write-project-manifest': 5.0.6 + fast-deep-equal: 3.1.3 + is-windows: 1.0.2 + json5: 2.2.3 + lodash.clonedeep: 4.5.0 + parse-json: 5.2.0 + read-yaml-file: 2.1.0 + sort-keys: 4.2.0 + strip-bom: 4.0.0 - '@ember-data/adapter@file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': + /@pnpm/render-peer-issues@4.0.1: + resolution: {integrity: sha512-+SsNmbBHH7lBsFrs6dQCEWRtT+Bmq9MYxu+xgkXRplyvjSEQmM0h/UduIw5s8ZAlUuQcxNVTvl0b7ul6OPEIwg==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + '@pnpm/types': 9.1.0 + archy: 1.0.0 + chalk: 4.1.2 + cli-columns: 4.0.0 - '@ember-data/adapter@file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': + /@pnpm/resolver-base@10.0.1: + resolution: {integrity: sha512-2yufLOpiPKQyNVLbL3dgoytkDuuURB5yBOrFtafiuZieGZJid2AeHmFfPhU9hNc/ZM1+wqH3EuVHe/1DdEgm4Q==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + '@pnpm/types': 9.1.0 - '@ember-data/adapter@file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': + /@pnpm/store-controller-types@15.0.1: + resolution: {integrity: sha512-S88sR6xhQ1ZDhMRIjhaRBA11N2OIDU2W+60szQLU8e2bw+KgGU60LbcXMunTdRnJskuB9UfDyoN6YuRtETBqYA==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + '@pnpm/fetcher-base': 14.0.1 + '@pnpm/resolver-base': 10.0.1 + '@pnpm/types': 9.1.0 - '@ember-data/adapter@file:packages/adapter(@ember-data/store@packages+store)(@ember/string@4.0.0)(ember-inflector@4.0.2)': + /@pnpm/text.comments-parser@2.0.0: + resolution: {integrity: sha512-DRWtTmmxQQtuWHf1xPt9bqzCSq8d0MQF5x1kdpCDMLd7xk3nP4To2/OGkPrb8MKbrWsgCNDwXyKCFlEKrAg7fg==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': link:packages/store - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + strip-comments-strings: 1.2.0 - '@ember-data/debug@file:packages/debug(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.77.0)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color - - webpack + /@pnpm/types@9.1.0: + resolution: {integrity: sha512-MMPDMLOY17bfNhLhR9Qmq6/2keoocnR5DWXZfZDC4dKXugrMsE1jB6RnuU8swJIo4zyCsMT/iVSAtl/XK+9Z+A==} + engines: {node: '>=16.14'} - '@ember-data/debug@file:packages/debug(@ember-data/store@4.12.8)(@ember/string@4.0.0)(webpack@5.77.0)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color - - webpack + /@pnpm/types@9.4.2: + resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} + engines: {node: '>=16.14'} - '@ember-data/graph@file:packages/graph(@ember-data/store@4.12.8)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/edition-utils': 1.2.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + /@pnpm/util.lex-comparator@1.0.0: + resolution: {integrity: sha512-3aBQPHntVgk5AweBWZn+1I/fqZ9krK/w01197aYVkAJQGftb+BVWgEepxY5GChjSW12j52XX+CmfynYZ/p0DFQ==} + engines: {node: '>=12.22.0'} - '@ember-data/json-api@file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8)': + /@pnpm/write-project-manifest@5.0.1: + resolution: {integrity: sha512-zU4vDfBUx/jUBPmR4CzCqPDOPObb/7iLT3UZvhXSJ8ZXDo9214V6agnJvxQ6bYBcypdiKva0hnb3tmo1chQBYg==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/edition-utils': 1.2.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + '@pnpm/text.comments-parser': 2.0.0 + '@pnpm/types': 9.1.0 + json5: 2.2.3 + write-file-atomic: 5.0.1 + write-yaml-file: 5.0.0 - '@ember-data/legacy-compat@file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1)': + /@pnpm/write-project-manifest@5.0.6: + resolution: {integrity: sha512-3qkKCftRE/HXzoWedyDuaMMUQzheDwx8AQXR0DnA9ylsBnZQYNut19Ado/gzi5+IvznaMcqrBszw57j3y1/ILw==} + engines: {node: '>=16.14'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - transitivePeerDependencies: - - supports-color + '@pnpm/text.comments-parser': 2.0.0 + '@pnpm/types': 9.4.2 + json5: 2.2.3 + write-file-atomic: 5.0.1 + write-yaml-file: 5.0.0 - '@ember-data/legacy-compat@file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0)': + /@rollup/plugin-babel@6.0.4(@babel/core@7.26.0)(rollup@4.25.0): + resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + rollup: + optional: true dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9(supports-color@8.1.1) + '@rollup/pluginutils': 5.1.3(rollup@4.25.0) + rollup: 4.25.0 transitivePeerDependencies: - supports-color - '@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0)': + /@rollup/pluginutils@4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: false - '@ember-data/model@file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /@rollup/pluginutils@5.1.3(rollup@4.25.0): + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - optionalDependencies: - '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.77.0) - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + rollup: 4.25.0 - '@ember-data/model@file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - optionalDependencies: - '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.8)(@ember/string@4.0.0)(webpack@5.77.0) - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-android-arm-eabi@4.25.0: + resolution: {integrity: sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true - '@ember-data/model@file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember/string@4.0.0) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - optionalDependencies: - '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.8)(@ember/string@4.0.0)(webpack@5.77.0) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-android-arm64@4.25.0: + resolution: {integrity: sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true - '@ember-data/model@file:packages/model(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-darwin-arm64@4.25.0: + resolution: {integrity: sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true - '@ember-data/model@file:packages/model(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/legacy-compat': link:packages/legacy-compat - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-darwin-x64@4.25.0: + resolution: {integrity: sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true - '@ember-data/model@file:packages/model(@babel/core@7.24.5)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/store@packages+store)(@ember-data/tracking@packages+tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0))': - dependencies: - '@ember-data/legacy-compat': link:packages/legacy-compat - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': link:packages/store - '@ember-data/tracking': link:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.24.5)(ember-source@4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - ember-cli-string-utils: 1.1.0 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - inflection: 2.0.1 - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-freebsd-arm64@4.25.0: + resolution: {integrity: sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true - '@ember-data/private-build-infra@file:packages/private-build-infra': - dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.5) - '@babel/runtime': 7.24.5 - '@ember/edition-utils': 1.2.0 - '@embroider/macros': 1.10.0 - babel-import-util: 1.3.0 - babel-plugin-debug-macros: 0.3.4(@babel/core@7.24.5) - babel-plugin-filter-imports: 4.0.0 - babel6-plugin-strip-class-callcheck: 6.0.0 - broccoli-debug: 0.6.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-rollup: 5.0.0 - calculate-cache-key-for-tree: 2.0.0 - chalk: 4.1.2 - ember-cli-babel: 7.26.11 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-version-checker: 5.1.2 - git-repo-info: 2.1.1 - glob: 9.3.4 - npm-git-info: 1.0.3 - semver: 7.6.0 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-freebsd-x64@4.25.0: + resolution: {integrity: sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true - '@ember-data/request@file:packages/request': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember/test-waiters': 3.0.2 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-arm-gnueabihf@4.25.0: + resolution: {integrity: sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/rfc395-data@0.0.4': {} + /@rollup/rollup-linux-arm-musleabihf@4.25.0: + resolution: {integrity: sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-arm64-gnu@4.25.0: + resolution: {integrity: sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-arm64-musl@4.25.0: + resolution: {integrity: sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-powerpc64le-gnu@4.25.0: + resolution: {integrity: sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-riscv64-gnu@4.25.0: + resolution: {integrity: sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@4.0.0)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-s390x-gnu@4.25.0: + resolution: {integrity: sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/serializer@file:packages/serializer(@ember-data/store@packages+store)(@ember/string@4.0.0)(ember-inflector@4.0.2)': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/store': link:packages/store - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - supports-color + /@rollup/rollup-linux-x64-gnu@4.25.0: + resolution: {integrity: sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-linux-x64-musl@4.25.0: + resolution: {integrity: sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@4.0.0) - '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-win32-arm64-msvc@4.25.0: + resolution: {integrity: sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.25.0: + resolution: {integrity: sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rollup/rollup-win32-x64-msvc@4.25.0: + resolution: {integrity: sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember/string@4.0.0) - '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/legacy-compat@file:packages/legacy-compat(@ember/string@4.0.0))(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + /@rtsao/scc@1.1.0: + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + dev: false - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /@rushstack/node-core-library@4.0.2: + resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/legacy-compat': link:packages/legacy-compat - '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/legacy-compat@packages+legacy-compat)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + z-schema: 5.0.5 + dev: false - '@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/tracking@file:packages/tracking)(@ember/string@4.0.0)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /@rushstack/rig-package@0.5.2: + resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/tracking': file:packages/tracking - '@ember/string': 4.0.0 - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - '@babel/core' - - ember-source - - supports-color + resolve: 1.22.8 + strip-json-comments: 3.1.1 + dev: false - '@ember-data/tracking@file:packages/tracking': + /@rushstack/terminal@0.10.0: + resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + '@rushstack/node-core-library': 4.0.2 + supports-color: 8.1.1 + dev: false - '@ember-data/unpublished-test-infra@file:packages/unpublished-test-infra': + /@rushstack/ts-command-line@4.19.1: + resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} dependencies: - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember/edition-utils': 1.2.0 - '@embroider/macros': 1.10.0 - broccoli-merge-trees: 4.2.0 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-cli-blueprint-test-helpers: 0.19.2 - ember-get-config: 2.1.1 - qunit: 2.19.4 - qunit-dom: 2.0.0 - semver: 7.6.0 - testem: 3.10.1 - webpack: 5.77.0 + '@rushstack/terminal': 0.10.0 + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 transitivePeerDependencies: - - '@swc/core' - - arc-templates - - atpl - - babel-core - - bracket-template - - bufferutil - - coffee-script - - debug - - dot - - dust - - dustjs-helpers - - dustjs-linkedin - - eco - - ect - - ejs - - esbuild - - haml-coffee - - hamlet - - hamljs - - handlebars - - hogan.js - - htmling - - jade - - jazz - - jqtpl - - just - - liquid-node - - liquor - - lodash - - marko - - mote - - nunjucks - - plates - - pug - - qejs - - ractive - - razor-tmpl - - react - - react-dom - - slm - - squirrelly - - supports-color - - swig - - swig-templates - - teacup - - templayed - - then-jade - - then-pug - - tinyliquid - - toffee - - twig - - twing - - uglify-js - - underscore - - utf-8-validate - - vash - - velocityjs - - walrus - - webpack-cli - - whiskers + - '@types/node' + dev: false - '@ember/edition-utils@1.2.0': {} - - '@ember/optional-features@2.0.0': - dependencies: - chalk: 4.1.2 - ember-cli-version-checker: 5.1.2 - glob: 7.2.3 - inquirer: 7.3.3 - mkdirp: 1.0.4 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + /@sec-ant/readable-stream@0.4.1: + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + dev: true - '@ember/string@3.1.1': + /@simple-dom/document@1.4.0: + resolution: {integrity: sha512-/RUeVH4kuD3rzo5/91+h4Z1meLSLP66eXqpVAw/4aZmYozkeqUkMprq0znL4psX/adEed5cBgiNJcfMz/eKZLg==} dependencies: - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + '@simple-dom/interface': 1.4.0 - '@ember/string@4.0.0': {} + /@simple-dom/interface@1.4.0: + resolution: {integrity: sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==} - '@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /@simple-dom/parser@1.4.0: + resolution: {integrity: sha512-TNjDkOehueRIKr1df416qk9ELj+qWuVVJNIT25y1aZg3pQvxv4UPGrgaDFte7dsWBTbF3V8NYPNQ5FDUZQ8Wlg==} dependencies: - '@ember/test-waiters': 3.0.2 - '@embroider/macros': 1.10.0 - '@embroider/util': 1.10.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - ember-cli-babel: 7.26.11 - ember-cli-htmlbars: 6.2.0 - ember-destroyable-polyfill: 2.0.3(@babel/core@7.21.4) - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - supports-color + '@simple-dom/interface': 1.4.0 + dev: true - '@ember/test-waiters@3.0.2': + /@simple-dom/serializer@1.4.0: + resolution: {integrity: sha512-mI1yRahsVs8atXLiQksineDsFEFqeG7RHwnnBTDOK6inbzl4tZQgjR+Z7edjgIJq5j5RhZvwPI6EuCji9B3eQw==} dependencies: - calculate-cache-key-for-tree: 2.0.0 - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color + '@simple-dom/interface': 1.4.0 + dev: true - '@embroider/addon-dev@3.0.0(rollup@3.20.2)': - dependencies: - '@embroider/core': 2.1.1 - '@rollup/pluginutils': 4.2.1 - assert-never: 1.2.1 - fs-extra: 10.1.0 - minimatch: 3.1.2 - rollup-plugin-copy-assets: 2.0.3(rollup@3.20.2) - rollup-plugin-delete: 2.0.0 - walk-sync: 3.0.0 - yargs: 17.7.1 - transitivePeerDependencies: - - bufferutil - - canvas - - rollup - - supports-color - - utf-8-validate + /@simple-dom/void-map@1.4.0: + resolution: {integrity: sha512-VDhLEyVCbuhOBBgHol9ShzIv9O8UCzdXeH4FoXu2DOcu/nnvTjLTck+BgXsCLv5ynDiUdoqsREEVFnoyPpFKVw==} + dev: true - '@embroider/addon-dev@4.3.1(rollup@4.17.2)': - dependencies: - '@embroider/core': 3.4.9 - '@rollup/pluginutils': 4.2.1 - content-tag: 2.0.1 - fs-extra: 10.1.0 - minimatch: 3.1.2 - rollup-plugin-copy-assets: 2.0.3(rollup@4.17.2) - rollup-plugin-delete: 2.0.0 - walk-sync: 3.0.0 - yargs: 17.7.1 - transitivePeerDependencies: - - bufferutil - - canvas - - rollup - - supports-color - - utf-8-validate + /@sindresorhus/is@0.14.0: + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + dev: true - '@embroider/babel-loader-8@2.0.0(@embroider/core@2.1.1)(supports-color@8.1.1)(webpack@5.77.0)': - dependencies: - '@babel/core': 7.24.5(supports-color@8.1.1) - '@embroider/core': 2.1.1 - babel-loader: 8.3.0(@babel/core@7.24.5(supports-color@8.1.1))(webpack@5.77.0) - transitivePeerDependencies: - - supports-color - - webpack + /@sindresorhus/merge-streams@2.3.0: + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + dev: true - '@embroider/compat@2.1.1(@embroider/core@2.1.1)': - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/core': 7.24.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) - '@babel/preset-env': 7.24.5(@babel/core@7.24.5) - '@babel/traverse': 7.21.3 - '@embroider/core': 2.1.1 - '@embroider/macros': 1.10.0 - '@types/babel__code-frame': 7.0.3 - '@types/yargs': 17.0.23 - assert-never: 1.2.1 - babel-plugin-ember-template-compilation: 2.0.0 - babel-plugin-syntax-dynamic-import: 6.18.0 - babylon: 6.18.0 - bind-decorator: 1.0.11 - broccoli: 3.5.2 - broccoli-concat: 4.2.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-persistent-filter: 3.1.3 - broccoli-plugin: 4.0.7 - broccoli-source: 3.0.1 - chalk: 4.1.2 - debug: 4.3.4 - fs-extra: 9.1.0 - fs-tree-diff: 2.0.1 - jsdom: 16.7.0 - lodash: 4.17.21 - pkg-up: 3.1.0 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - semver: 7.3.8 - symlink-or-copy: 1.3.1 - tree-sync: 2.1.0 - typescript-memoize: 1.1.1 - walk-sync: 3.0.0 - yargs: 17.7.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate + /@sindresorhus/merge-streams@4.0.0: + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + dev: true - '@embroider/core@2.1.1': - dependencies: - '@babel/core': 7.24.5 - '@babel/parser': 7.21.3 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-transform-runtime': 7.24.3(@babel/core@7.24.5) - '@babel/runtime': 7.24.5 - '@babel/traverse': 7.21.3 - '@embroider/macros': 1.10.0 - '@embroider/shared-internals': 2.0.0 - assert-never: 1.2.1 - babel-import-util: 1.3.0 - babel-plugin-ember-template-compilation: 2.0.0 - broccoli-node-api: 1.7.0 - broccoli-persistent-filter: 3.1.3 - broccoli-plugin: 4.0.7 - broccoli-source: 3.0.1 - debug: 4.3.4 - escape-string-regexp: 4.0.0 - fast-sourcemap-concat: 1.4.0 - filesize: 5.0.3 - fs-extra: 9.1.0 - fs-tree-diff: 2.0.1 - handlebars: 4.7.7 - js-string-escape: 1.0.1 - jsdom: 16.7.0 - lodash: 4.17.21 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - typescript-memoize: 1.1.1 - walk-sync: 3.0.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@embroider/core@3.4.9': + /@szmarczak/http-timer@1.1.2: + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} dependencies: - '@babel/core': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/traverse': 7.24.5 - '@embroider/macros': 1.10.0 - '@embroider/shared-internals': 2.6.0 - assert-never: 1.2.1 - babel-plugin-ember-template-compilation: 2.2.2 - broccoli-node-api: 1.7.0 - broccoli-persistent-filter: 3.1.3 - broccoli-plugin: 4.0.7 - broccoli-source: 3.0.1 - debug: 4.3.4 - fast-sourcemap-concat: 1.4.0 - filesize: 10.1.1 - fs-extra: 9.1.0 - fs-tree-diff: 2.0.1 - handlebars: 4.7.7 - js-string-escape: 1.0.1 - jsdom: 16.7.0 - lodash: 4.17.21 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - typescript-memoize: 1.1.1 - walk-sync: 3.0.0 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate + defer-to-connect: 1.1.3 + dev: true - '@embroider/hbs-loader@2.0.0(@embroider/core@2.1.1)(webpack@5.77.0)': - dependencies: - '@embroider/core': 2.1.1 - webpack: 5.77.0 + /@tootallnate/once@1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + dev: true - '@embroider/macros@1.10.0': - dependencies: - '@embroider/shared-internals': 2.0.0 - assert-never: 1.2.1 - babel-import-util: 1.3.0 - ember-cli-babel: 7.26.11 - find-up: 5.0.0 - lodash: 4.17.21 - resolve: 1.22.1 - semver: 7.3.8 - transitivePeerDependencies: - - supports-color + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true - '@embroider/shared-internals@2.0.0': - dependencies: - babel-import-util: 1.3.0 - ember-rfc176-data: 0.3.18 - fs-extra: 9.1.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - resolve-package-path: 4.0.3 - semver: 7.3.8 - typescript-memoize: 1.1.1 + /@tsconfig/ember@3.0.8: + resolution: {integrity: sha512-OVnIsZIt/8q0VEtcdz3rRryNrm6gdJTxXlxefkGIrkZnME0wqslmwHlUEZ7mvh377df9FqBhNKrYNarhCW8zJA==} + dev: true - '@embroider/shared-internals@2.6.0': - dependencies: - babel-import-util: 2.1.1 - debug: 4.3.4 - ember-rfc176-data: 0.3.18 - fs-extra: 9.1.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - minimatch: 3.1.2 - resolve-package-path: 4.0.3 - semver: 7.6.0 - typescript-memoize: 1.1.1 - transitivePeerDependencies: - - supports-color + /@types/argparse@1.0.38: + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + dev: false - '@embroider/util@1.10.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': - dependencies: - '@embroider/macros': 1.10.0 - broccoli-funnel: 3.0.8 - ember-cli-babel: 7.26.11 - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - transitivePeerDependencies: - - supports-color + /@types/babel__code-frame@7.0.6: + resolution: {integrity: sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==} + dev: true - '@embroider/webpack@2.1.1(@embroider/core@2.1.1)(webpack@5.77.0)': + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/core': 7.24.5(supports-color@8.1.1) - '@embroider/babel-loader-8': 2.0.0(@embroider/core@2.1.1)(supports-color@8.1.1)(webpack@5.77.0) - '@embroider/core': 2.1.1 - '@embroider/hbs-loader': 2.0.0(@embroider/core@2.1.1)(webpack@5.77.0) - '@embroider/shared-internals': 2.0.0 - '@types/source-map': 0.5.7 - '@types/supports-color': 8.1.1 - babel-loader: 8.3.0(@babel/core@7.24.5(supports-color@8.1.1))(webpack@5.77.0) - babel-preset-env: 1.7.0(supports-color@8.1.1) - css-loader: 5.2.7(webpack@5.77.0) - csso: 4.2.0 - debug: 4.3.4(supports-color@8.1.1) - fs-extra: 9.1.0 - jsdom: 16.7.0(supports-color@8.1.1) - lodash: 4.17.21 - mini-css-extract-plugin: 2.7.5(webpack@5.77.0) - semver: 7.3.8 - source-map-url: 0.4.1 - style-loader: 2.0.0(webpack@5.77.0) - supports-color: 8.1.1 - terser: 5.16.8 - thread-loader: 3.0.4(webpack@5.77.0) - webpack: 5.77.0 - transitivePeerDependencies: - - bufferutil - - canvas - - utf-8-validate + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + dev: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.37.0)': + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: - eslint: 8.37.0 - eslint-visitor-keys: 3.4.0 + '@babel/types': 7.26.0 + dev: true - '@eslint-community/regexpp@4.4.1': {} + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + dev: true - '@eslint/eslintrc@2.0.2': + /@types/babel__traverse@7.20.6: + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.5.1 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@babel/types': 7.26.0 + dev: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.17.6 - '@eslint/js@8.37.0': {} + /@types/chai-as-promised@7.1.8: + resolution: {integrity: sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==} + dependencies: + '@types/chai': 4.3.20 - '@gar/promisify@1.1.3': {} + /@types/chai@4.3.20: + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} - '@glimmer/compiler@0.87.1': + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/syntax': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/vm': 0.87.1 - '@glimmer/wire-format': 0.87.1 + '@types/node': 20.17.6 - '@glimmer/component@1.1.2(@babel/core@7.21.4)': + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} dependencies: - '@glimmer/di': 0.1.11 - '@glimmer/env': 0.1.7 - '@glimmer/util': 0.44.0 - broccoli-file-creator: 2.1.1 - broccoli-merge-trees: 4.2.0 - ember-cli-babel: 7.26.11 - ember-cli-get-component-path-option: 1.0.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-typescript: 3.0.0(@babel/core@7.21.4) - ember-cli-version-checker: 3.1.3 - ember-compatibility-helpers: 1.2.6(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@types/node': 20.17.6 - '@glimmer/component@1.1.2(@babel/core@7.24.5)': + /@types/eslint@8.56.12: + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} dependencies: - '@glimmer/di': 0.1.11 - '@glimmer/env': 0.1.7 - '@glimmer/util': 0.44.0 - broccoli-file-creator: 2.1.1 - broccoli-merge-trees: 4.2.0 - ember-cli-babel: 7.26.11 - ember-cli-get-component-path-option: 1.0.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-typescript: 3.0.0(@babel/core@7.24.5) - ember-cli-version-checker: 3.1.3 - ember-compatibility-helpers: 1.2.6(@babel/core@7.24.5) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + dev: true - '@glimmer/debug@0.87.1': + /@types/eslint@9.6.1: + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/vm': 0.87.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + dev: true - '@glimmer/destroyable@0.87.1': + /@types/eslint__js@8.42.3: + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 + '@types/eslint': 9.6.1 + dev: true - '@glimmer/di@0.1.11': {} + /@types/estree@1.0.6: + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@glimmer/encoder@0.87.1': + /@types/express-serve-static-core@4.19.6: + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/vm': 0.87.1 - - '@glimmer/env@0.1.7': {} + '@types/node': 20.17.6 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 - '@glimmer/global-context@0.87.1': {} + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 - '@glimmer/interfaces@0.84.3': + /@types/fs-extra@8.1.5: + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} dependencies: - '@simple-dom/interface': 1.4.0 + '@types/node': 20.17.6 - '@glimmer/interfaces@0.87.1': + /@types/glob@7.2.0: + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: - '@simple-dom/interface': 1.4.0 + '@types/minimatch': 5.1.2 + '@types/node': 20.17.6 - '@glimmer/manager@0.87.1': + /@types/glob@8.1.0: + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} dependencies: - '@glimmer/debug': 0.87.1 - '@glimmer/destroyable': 0.87.1 - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/reference': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/validator': 0.87.1 - '@glimmer/vm': 0.87.1 - - '@glimmer/node@0.87.1': - dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/runtime': 0.87.1 - '@glimmer/util': 0.87.1 - '@simple-dom/document': 1.4.0 + '@types/minimatch': 5.1.2 + '@types/node': 20.17.6 + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@glimmer/opcode-compiler@0.87.1': + /@types/jquery@3.5.32: + resolution: {integrity: sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==} dependencies: - '@glimmer/debug': 0.87.1 - '@glimmer/encoder': 0.87.1 - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/manager': 0.87.1 - '@glimmer/reference': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/vm': 0.87.1 - '@glimmer/wire-format': 0.87.1 + '@types/sizzle': 2.3.9 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@glimmer/owner@0.87.1': + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: false + + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@glimmer/util': 0.87.1 + '@types/node': 20.17.6 + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + /@types/minimatch@3.0.5: + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@glimmer/program@0.87.1': + /@types/node@20.12.14: + resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} dependencies: - '@glimmer/encoder': 0.87.1 - '@glimmer/env': 0.1.7 - '@glimmer/interfaces': 0.87.1 - '@glimmer/manager': 0.87.1 - '@glimmer/opcode-compiler': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/vm': 0.87.1 - '@glimmer/wire-format': 0.87.1 + undici-types: 5.26.5 + dev: true - '@glimmer/reference@0.87.1': + /@types/node@20.17.6: + resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/validator': 0.87.1 + undici-types: 6.19.8 + + /@types/qs@6.9.17: + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} + + /@types/qunit@2.19.10: + resolution: {integrity: sha512-gVB+rxvxmbyPFWa6yjjKgcumWal3hyqoTXI0Oil161uWfo1OCzWZ/rnEumsx+6uVgrwPrCrhpQbLkzfildkSbg==} + dev: true - '@glimmer/runtime@0.87.1': + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@glimmer/destroyable': 0.87.1 - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/manager': 0.87.1 - '@glimmer/owner': 0.87.1 - '@glimmer/program': 0.87.1 - '@glimmer/reference': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/validator': 0.87.1 - '@glimmer/vm': 0.87.1 - '@glimmer/wire-format': 0.87.1 - - '@glimmer/syntax@0.84.3': + '@types/node': 20.17.6 + dev: true + + /@types/rimraf@2.0.5: + resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} dependencies: - '@glimmer/interfaces': 0.84.3 - '@glimmer/util': 0.84.3 - '@handlebars/parser': 2.0.0 - simple-html-tokenizer: 0.5.11 + '@types/glob': 8.1.0 + '@types/node': 20.17.6 + + /@types/rsvp@4.0.9: + resolution: {integrity: sha512-F6vaN5mbxw2MBCu/AD9fSKwrhnto2pE77dyUsi415qz9IP9ni9ZOWXHxnXfsM4NW9UjW+it189jvvqnhv37Z7Q==} + dev: true - '@glimmer/syntax@0.87.1': + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/wire-format': 0.87.1 - '@handlebars/parser': 2.0.0 - simple-html-tokenizer: 0.5.11 + '@types/mime': 1.3.5 + '@types/node': 20.17.6 - '@glimmer/tracking@1.1.2': + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/validator': 0.44.0 + '@types/http-errors': 2.0.4 + '@types/node': 20.17.6 + '@types/send': 0.17.4 - '@glimmer/util@0.44.0': {} + /@types/sizzle@2.3.9: + resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} + dev: true - '@glimmer/util@0.84.3': + /@types/ssri@7.1.5: + resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==} dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/interfaces': 0.84.3 - '@simple-dom/interface': 1.4.0 + '@types/node': 20.17.6 - '@glimmer/util@0.87.1': - dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/interfaces': 0.87.1 + /@types/supports-color@8.1.3: + resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} + dev: true - '@glimmer/validator@0.44.0': {} + /@types/symlink-or-copy@1.2.2: + resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} - '@glimmer/validator@0.87.1': + /@types/tmp@0.0.33: + resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} + dev: true + + /@types/ws@8.5.13: + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 + '@types/node': 20.17.6 + dev: true + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true - '@glimmer/vm-babel-plugins@0.84.2(@babel/core@7.21.4)': + /@types/yargs@17.0.33: + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} dependencies: - babel-plugin-debug-macros: 0.3.4(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' + '@types/yargs-parser': 21.0.3 + dev: true - '@glimmer/vm-babel-plugins@0.84.2(@babel/core@7.24.5)': + /@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0)(typescript@5.6.3): + resolution: {integrity: sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true dependencies: - babel-plugin-debug-macros: 0.3.4(@babel/core@7.24.5) + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/type-utils': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + eslint: 9.14.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.6.3) + typescript: 5.6.3 transitivePeerDependencies: - - '@babel/core' + - supports-color - '@glimmer/vm-babel-plugins@0.87.1(@babel/core@7.24.5)': + /@typescript-eslint/parser@8.14.0(eslint@9.14.0)(typescript@5.6.3): + resolution: {integrity: sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true dependencies: - babel-plugin-debug-macros: 0.3.4(@babel/core@7.24.5) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7(supports-color@8.1.1) + eslint: 9.14.0 + typescript: 5.6.3 transitivePeerDependencies: - - '@babel/core' + - supports-color - '@glimmer/vm@0.87.1': + /@typescript-eslint/scope-manager@8.14.0: + resolution: {integrity: sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 - '@glimmer/wire-format@0.87.1': + /@typescript-eslint/type-utils@8.14.0(eslint@9.14.0)(typescript@5.6.3): + resolution: {integrity: sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true dependencies: - '@glimmer/interfaces': 0.87.1 - '@glimmer/util': 0.87.1 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + debug: 4.3.7(supports-color@8.1.1) + ts-api-utils: 1.4.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color - '@gwhitney/detect-indent@7.0.1': {} + /@typescript-eslint/types@8.14.0: + resolution: {integrity: sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@handlebars/parser@2.0.0': {} + /@typescript-eslint/typescript-estree@8.14.0(typescript@5.6.3): + resolution: {integrity: sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7(supports-color@8.1.1) + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color - '@humanwhocodes/config-array@0.11.8': + /@typescript-eslint/utils@8.14.0(eslint@9.14.0)(typescript@5.6.3): + resolution: {integrity: sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + eslint: 9.14.0 transitivePeerDependencies: - supports-color + - typescript - '@humanwhocodes/module-importer@1.0.1': {} + /@typescript-eslint/visitor-keys@8.14.0: + resolution: {integrity: sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.14.0 + eslint-visitor-keys: 3.4.3 - '@humanwhocodes/object-schema@1.2.1': {} + /@volar/language-core@1.11.1: + resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + dependencies: + '@volar/source-map': 1.11.1 + dev: false - '@jridgewell/gen-mapping@0.1.1': + /@volar/source-map@1.11.1: + resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + muggle-string: 0.3.1 + dev: false - '@jridgewell/gen-mapping@0.3.2': + /@volar/typescript@1.11.1: + resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 + '@volar/language-core': 1.11.1 + path-browserify: 1.0.1 + dev: false - '@jridgewell/gen-mapping@0.3.5': + /@vue/compiler-core@3.5.12: + resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==} dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 7.26.2 + '@vue/shared': 3.5.12 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + dev: false - '@jridgewell/resolve-uri@3.1.0': {} + /@vue/compiler-dom@3.5.12: + resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==} + dependencies: + '@vue/compiler-core': 3.5.12 + '@vue/shared': 3.5.12 + dev: false - '@jridgewell/set-array@1.1.2': {} + /@vue/language-core@1.8.27(typescript@5.6.3): + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@volar/language-core': 1.11.1 + '@volar/source-map': 1.11.1 + '@vue/compiler-dom': 3.5.12 + '@vue/shared': 3.5.12 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.3.1 + path-browserify: 1.0.1 + typescript: 5.6.3 + vue-template-compiler: 2.7.16 + dev: false - '@jridgewell/set-array@1.2.1': {} + /@vue/shared@3.5.12: + resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==} + dev: false - '@jridgewell/source-map@0.3.2': + /@webassemblyjs/ast@1.14.1: + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@jridgewell/sourcemap-codec@1.4.14': {} + /@webassemblyjs/floating-point-hex-parser@1.13.2: + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - '@jridgewell/trace-mapping@0.3.17': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + /@webassemblyjs/helper-api-error@1.13.2: + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - '@jridgewell/trace-mapping@0.3.25': + /@webassemblyjs/helper-buffer@1.14.1: + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + /@webassemblyjs/helper-numbers@1.13.2: + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': - optional: true + /@webassemblyjs/helper-wasm-bytecode@1.13.2: + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + /@webassemblyjs/helper-wasm-section@1.14.1: + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} dependencies: - eslint-scope: 5.1.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 - '@nodelib/fs.scandir@2.1.5': + /@webassemblyjs/ieee754@1.13.2: + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} + '@xtuc/ieee754': 1.2.0 - '@nodelib/fs.walk@1.2.8': + /@webassemblyjs/leb128@1.13.2: + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 + '@xtuc/long': 4.2.2 - '@npmcli/fs@1.1.1': - dependencies: - '@gar/promisify': 1.1.3 - semver: 7.6.0 + /@webassemblyjs/utf8@1.13.2: + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - '@npmcli/move-file@1.1.2': + /@webassemblyjs/wasm-edit@1.14.1: + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} dependencies: - mkdirp: 1.0.4 - rimraf: 3.0.2 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 - '@pnpm/cli-meta@5.0.1': + /@webassemblyjs/wasm-gen@1.14.1: + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} dependencies: - '@pnpm/types': 9.1.0 - load-json-file: 6.2.0 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - '@pnpm/cli-utils@2.0.9(@pnpm/logger@5.0.0)': + /@webassemblyjs/wasm-opt@1.14.1: + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} dependencies: - '@pnpm/cli-meta': 5.0.1 - '@pnpm/config': 18.4.0(@pnpm/logger@5.0.0) - '@pnpm/default-reporter': 12.2.3(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.1 - '@pnpm/logger': 5.0.0 - '@pnpm/manifest-utils': 5.0.1(@pnpm/logger@5.0.0) - '@pnpm/package-is-installable': 8.0.2(@pnpm/logger@5.0.0) - '@pnpm/read-project-manifest': 5.0.1 - '@pnpm/types': 9.1.0 - chalk: 4.1.2 - load-json-file: 6.2.0 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 - '@pnpm/config.env-replace@1.1.0': {} + /@webassemblyjs/wasm-parser@1.14.1: + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - '@pnpm/config@18.4.0(@pnpm/logger@5.0.0)': + /@webassemblyjs/wast-printer@1.14.1: + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} dependencies: - '@pnpm/config.env-replace': 1.1.0 - '@pnpm/constants': 7.1.0 - '@pnpm/error': 5.0.1 - '@pnpm/git-utils': 1.0.0 - '@pnpm/matcher': 5.0.0 - '@pnpm/npm-conf': 2.2.0 - '@pnpm/pnpmfile': 5.0.7(@pnpm/logger@5.0.0) - '@pnpm/read-project-manifest': 5.0.1 - '@pnpm/types': 9.1.0 - better-path-resolve: 1.0.0 - camelcase: 6.3.0 - camelcase-keys: 6.2.2 - can-write-to-dir: 1.1.1 - is-subdir: 1.2.0 - is-windows: 1.0.2 - normalize-registry-url: 2.0.0 - path-absolute: 1.0.1 - path-name: 1.0.0 - ramda: '@pnpm/ramda@0.28.1' - read-ini-file: 4.0.0 - realpath-missing: 1.1.0 - which: 3.0.1 - transitivePeerDependencies: - - '@pnpm/logger' + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 - '@pnpm/constants@7.1.0': {} + /@xmldom/xmldom@0.8.10: + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} - '@pnpm/constants@7.1.1': {} + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - '@pnpm/core-loggers@9.0.1(@pnpm/logger@5.0.0)': - dependencies: - '@pnpm/logger': 5.0.0 - '@pnpm/types': 9.1.0 + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@pnpm/dedupe.issues-renderer@1.0.0': + /@zkochan/which@2.0.3: + resolution: {integrity: sha512-C1ReN7vt2/2O0fyTsx5xnbQuxBrmG5NMSbcIkPKCCfCTJgpZBsuRYzFXHj3nVq8vTfK7vxHUmzfCpSHgO7j4rg==} + engines: {node: '>= 8'} + hasBin: true dependencies: - '@pnpm/dedupe.types': 1.0.0 - archy: 1.0.0 - chalk: 4.1.2 + isexe: 2.0.0 - '@pnpm/dedupe.types@1.0.0': {} + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + dev: true - '@pnpm/default-reporter@12.2.3(@pnpm/logger@5.0.0)': - dependencies: - '@pnpm/config': 18.4.0(@pnpm/logger@5.0.0) - '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.0.0) - '@pnpm/dedupe.issues-renderer': 1.0.0 - '@pnpm/dedupe.types': 1.0.0 - '@pnpm/error': 5.0.1 - '@pnpm/logger': 5.0.0 - '@pnpm/render-peer-issues': 4.0.1 - '@pnpm/types': 9.1.0 - ansi-diff: 1.1.1 - boxen: 5.1.2 - chalk: 4.1.2 - normalize-path: 3.0.0 - pretty-bytes: 5.6.0 - pretty-ms: 7.0.1 - ramda: '@pnpm/ramda@0.28.1' - right-pad: 1.0.1 - rxjs: 7.8.1 - semver: 7.6.0 - stacktracey: 2.1.8 - string-length: 4.0.2 - strip-ansi: 6.0.1 + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - '@pnpm/error@5.0.1': + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} dependencies: - '@pnpm/constants': 7.1.0 + mime-types: 2.1.35 + negotiator: 0.6.3 - '@pnpm/error@5.0.3': + /acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} dependencies: - '@pnpm/constants': 7.1.1 + acorn: 7.4.1 + acorn-walk: 7.2.0 + dev: true - '@pnpm/fetcher-base@14.0.1': + /acorn-import-attributes@1.9.5(acorn@8.14.0): + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 dependencies: - '@pnpm/resolver-base': 10.0.1 - '@pnpm/types': 9.1.0 - '@types/ssri': 7.1.5 + acorn: 8.14.0 - '@pnpm/find-workspace-dir@6.0.3': + /acorn-jsx@5.3.2(acorn@8.14.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@pnpm/error': 5.0.3 - find-up: 5.0.0 + acorn: 8.14.0 + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true - '@pnpm/find-workspace-packages@6.0.9(@pnpm/logger@5.0.0)': + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} dependencies: - '@pnpm/cli-utils': 2.0.9(@pnpm/logger@5.0.0) - '@pnpm/constants': 7.1.0 - '@pnpm/fs.find-packages': 2.0.1 - '@pnpm/types': 9.1.0 - '@pnpm/util.lex-comparator': 1.0.0 - read-yaml-file: 2.1.0 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - - '@pnpm/logger' + - supports-color + dev: true - '@pnpm/fs.find-packages@2.0.1': + /agent-base@7.1.1(supports-color@8.1.1): + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} dependencies: - '@pnpm/read-project-manifest': 5.0.1 - '@pnpm/types': 9.1.0 - '@pnpm/util.lex-comparator': 1.0.0 - fast-glob: 3.2.12 - p-filter: 2.1.0 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - '@pnpm/fs.hard-link-dir@2.0.1(@pnpm/logger@5.0.0)': + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} dependencies: - '@pnpm/logger': 5.0.0 + humanize-ms: 1.2.1 + dev: true - '@pnpm/git-utils@1.0.0': + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} dependencies: - execa: safe-execa@0.1.2 + clean-stack: 2.2.0 + indent-string: 4.0.0 - '@pnpm/graceful-fs@3.0.0': + /ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} dependencies: - graceful-fs: 4.2.11 + ajv: 8.17.1 - '@pnpm/graceful-fs@3.2.0': + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 dependencies: - graceful-fs: 4.2.11 + ajv: 6.12.6 - '@pnpm/hooks.types@1.0.1': + /ajv-keywords@5.1.0(ajv@8.17.1): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 dependencies: - '@pnpm/lockfile-types': 5.1.0 - '@pnpm/types': 9.1.0 + ajv: 8.17.1 + fast-deep-equal: 3.1.3 - '@pnpm/lockfile-types@5.1.0': + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: - '@pnpm/types': 9.1.0 + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 - '@pnpm/logger@5.0.0': + /ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} dependencies: - bole: 5.0.12 - ndjson: 2.0.0 + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 - '@pnpm/manifest-utils@5.0.1(@pnpm/logger@5.0.0)': + /amd-name-resolver@1.3.1: + resolution: {integrity: sha512-26qTEWqZQ+cxSYygZ4Cf8tsjDBLceJahhtewxtKZA3SRa4PluuqYCuheemDQD+7Mf5B7sr+zhTDWAHDh02a1Dw==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.1 - '@pnpm/types': 9.1.0 - transitivePeerDependencies: - - '@pnpm/logger' + ensure-posix-path: 1.1.1 + object-hash: 1.3.1 - '@pnpm/matcher@5.0.0': - dependencies: - escape-string-regexp: 4.0.0 + /amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} - '@pnpm/network.ca-file@1.0.2': + /anafanafo@2.0.0: + resolution: {integrity: sha512-Nlfq7NC4AOkTJerWRIZcOAiMNtIDVIGWGvQ98O7Jl6Kr2Dk0dX5u4MqN778kSRTy5KRqchpLdF2RtLFEz9FVkQ==} dependencies: - graceful-fs: 4.2.10 + char-width-table-consumer: 1.0.0 + dev: true - '@pnpm/npm-conf@2.2.0': + /ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} dependencies: - '@pnpm/config.env-replace': 1.1.0 - '@pnpm/network.ca-file': 1.0.2 - config-chain: 1.1.13 + string-width: 4.2.3 - '@pnpm/package-is-installable@8.0.2(@pnpm/logger@5.0.0)': - dependencies: - '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.1 - '@pnpm/logger': 5.0.0 - '@pnpm/types': 9.1.0 - detect-libc: 2.0.3 - execa: safe-execa@0.1.2 - mem: 8.1.1 - semver: 7.6.0 + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true - '@pnpm/pnpmfile@5.0.7(@pnpm/logger@5.0.0)': + /ansi-diff@1.2.0: + resolution: {integrity: sha512-BIXwHKpjzghBjcwEV10Y4b17tjHfK4nhEqK3LqyQ3JgcMcjmi3DIevozNgrOpfvBMmrq9dfvrPJSu5/5vNUBQg==} dependencies: - '@pnpm/core-loggers': 9.0.1(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.1 - '@pnpm/hooks.types': 1.0.1 - '@pnpm/lockfile-types': 5.1.0 - '@pnpm/logger': 5.0.0 - '@pnpm/store-controller-types': 15.0.1 - '@pnpm/types': 9.1.0 - chalk: 4.1.2 - path-absolute: 1.0.1 + ansi-split: 1.0.1 + wcwidth: 1.0.1 - '@pnpm/ramda@0.28.1': {} + /ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} - '@pnpm/read-project-manifest@5.0.1': + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} dependencies: - '@gwhitney/detect-indent': 7.0.1 - '@pnpm/error': 5.0.1 - '@pnpm/graceful-fs': 3.0.0 - '@pnpm/text.comments-parser': 2.0.0 - '@pnpm/types': 9.1.0 - '@pnpm/write-project-manifest': 5.0.1 - fast-deep-equal: 3.1.3 - is-windows: 1.0.2 - json5: 2.2.3 - parse-json: 5.2.0 - read-yaml-file: 2.1.0 - sort-keys: 4.2.0 - strip-bom: 4.0.0 + type-fest: 0.21.3 - '@pnpm/read-project-manifest@5.0.11': - dependencies: - '@gwhitney/detect-indent': 7.0.1 - '@pnpm/error': 5.0.3 - '@pnpm/graceful-fs': 3.2.0 - '@pnpm/text.comments-parser': 2.0.0 - '@pnpm/types': 9.4.2 - '@pnpm/write-project-manifest': 5.0.6 - fast-deep-equal: 3.1.3 - is-windows: 1.0.2 - json5: 2.2.3 - lodash.clonedeep: 4.5.0 - parse-json: 5.2.0 - read-yaml-file: 2.1.0 - sort-keys: 4.2.0 - strip-bom: 4.0.0 + /ansi-html@0.0.7: + resolution: {integrity: sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==} + engines: {'0': node >= 0.8.0} + hasBin: true - '@pnpm/render-peer-issues@4.0.1': - dependencies: - '@pnpm/types': 9.1.0 - archy: 1.0.0 - chalk: 4.1.2 - cli-columns: 4.0.0 + /ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: true - '@pnpm/resolver-base@10.0.1': - dependencies: - '@pnpm/types': 9.1.0 + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} - '@pnpm/store-controller-types@15.0.1': - dependencies: - '@pnpm/fetcher-base': 14.0.1 - '@pnpm/resolver-base': 10.0.1 - '@pnpm/types': 9.1.0 + /ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} - '@pnpm/text.comments-parser@2.0.0': - dependencies: - strip-comments-strings: 1.2.0 + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} - '@pnpm/types@9.1.0': {} + /ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + dev: true - '@pnpm/types@9.4.2': {} + /ansi-split@1.0.1: + resolution: {integrity: sha512-RRxQym4DFtDNmHIkW6aeFVvrXURb11lGAEPXNiryjCe8bK8RsANjzJ0M2aGOkvBYwP4Bl/xZ8ijtr6D3j1x/eg==} + dependencies: + ansi-regex: 3.0.1 - '@pnpm/util.lex-comparator@1.0.0': {} + /ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + dev: true - '@pnpm/write-project-manifest@5.0.1': + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} dependencies: - '@pnpm/text.comments-parser': 2.0.0 - '@pnpm/types': 9.1.0 - json5: 2.2.3 - write-file-atomic: 5.0.1 - write-yaml-file: 5.0.0 + color-convert: 1.9.3 - '@pnpm/write-project-manifest@5.0.6': + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} dependencies: - '@pnpm/text.comments-parser': 2.0.0 - '@pnpm/types': 9.4.2 - json5: 2.2.3 - write-file-atomic: 5.0.1 - write-yaml-file: 5.0.0 + color-convert: 2.0.1 - '@rollup/plugin-babel@6.0.3(@babel/core@7.21.4)(rollup@3.20.2)': - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-module-imports': 7.18.6 - '@rollup/pluginutils': 5.0.2(rollup@3.20.2) - optionalDependencies: - rollup: 3.20.2 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true - '@rollup/plugin-babel@6.0.4(@babel/core@7.24.5)(rollup@4.17.2)': + /ansi-to-html@0.6.15: + resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==} + engines: {node: '>=8.0.0'} + hasBin: true dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-imports': 7.24.3 - '@rollup/pluginutils': 5.0.2(rollup@4.17.2) - optionalDependencies: - rollup: 4.17.2 + entities: 2.2.0 - '@rollup/plugin-node-resolve@15.0.1(rollup@3.20.2)': - dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.20.2) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-builtin-module: 3.2.1 - is-module: 1.0.0 - resolve: 1.22.1 - optionalDependencies: - rollup: 3.20.2 + /ansicolors@0.2.1: + resolution: {integrity: sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==} + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true - '@rollup/plugin-node-resolve@15.2.3(rollup@4.17.2)': + /anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} dependencies: - '@rollup/pluginutils': 5.0.2(rollup@4.17.2) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-builtin-module: 3.2.1 - is-module: 1.0.0 - resolve: 1.22.1 - optionalDependencies: - rollup: 4.17.2 + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color - '@rollup/pluginutils@4.2.1': + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} dependencies: - estree-walker: 2.0.2 + normalize-path: 3.0.0 picomatch: 2.3.1 - '@rollup/pluginutils@5.0.2(rollup@3.20.2)': + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + + /are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. dependencies: - '@types/estree': 1.0.0 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 3.20.2 + delegates: 1.0.0 + readable-stream: 3.6.2 - '@rollup/pluginutils@5.0.2(rollup@4.17.2)': + /are-we-there-yet@4.0.2: + resolution: {integrity: sha512-ncSWAawFhKMJDTdoAeOV+jyW1VCMj5QIAwULIBV0SSR7B/RLPPEQiknKcg/RIIZlUQrxELpsxMiTUoAQ4sIUyg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: - '@types/estree': 1.0.0 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 4.17.2 + sprintf-js: 1.0.3 - '@rollup/rollup-android-arm-eabi@4.17.2': - optional: true + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - '@rollup/rollup-android-arm64@4.17.2': - optional: true + /aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + dev: true - '@rollup/rollup-darwin-arm64@4.17.2': - optional: true + /arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} - '@rollup/rollup-darwin-x64@4.17.2': - optional: true + /arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': - optional: true + /arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} - '@rollup/rollup-linux-arm-musleabihf@4.17.2': - optional: true + /array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: true - '@rollup/rollup-linux-arm64-gnu@4.17.2': - optional: true + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 - '@rollup/rollup-linux-arm64-musl@4.17.2': - optional: true + /array-equal@1.0.2: + resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': - optional: true + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - '@rollup/rollup-linux-riscv64-gnu@4.17.2': - optional: true + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + dev: false - '@rollup/rollup-linux-s390x-gnu@4.17.2': - optional: true + /array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true - '@rollup/rollup-linux-x64-gnu@4.17.2': - optional: true + /array-to-error@1.1.1: + resolution: {integrity: sha512-kqcQ8s7uQfg3UViYON3kCMcck3A9exxgq+riVuKy08Mx00VN4EJhK30L2VpjE58LQHKhcE/GRpvbVUhqTvqzGQ==} + dependencies: + array-to-sentence: 1.1.0 + dev: true + + /array-to-sentence@1.1.0: + resolution: {integrity: sha512-YkwkMmPA2+GSGvXj1s9NZ6cc2LBtR+uSeWTy2IGi5MR1Wag4DdrcjTxA/YV/Fw+qKlBeXomneZgThEbm/wvZbw==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + /array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + /array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + dev: false + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-shim-unscopables: 1.0.2 + dev: false + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-shim-unscopables: 1.0.2 + dev: false + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + /as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + dependencies: + printable-characters: 1.0.42 - '@rollup/rollup-linux-x64-musl@4.17.2': + /asn1@0.1.11: + resolution: {integrity: sha512-Fh9zh3G2mZ8qM/kwsiKwL2U2FmXxVsboP4x1mXjnhKHv3SmzaBZoYvxEQJz/YS2gnCgd8xlAVWcZnQyC9qZBsA==} + engines: {node: '>=0.4.9'} + requiresBuild: true + dev: true optional: true - '@rollup/rollup-win32-arm64-msvc@4.17.2': - optional: true + /assert-never@1.3.0: + resolution: {integrity: sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==} - '@rollup/rollup-win32-ia32-msvc@4.17.2': + /assert-plus@0.1.5: + resolution: {integrity: sha512-brU24g7ryhRwGCI2y+1dGQmQXiZF7TtIj583S96y0jjdajIe6wn8BuXyELYhvD22dtIxDQVFk04YTJwwdwOYJw==} + engines: {node: '>=0.8'} + requiresBuild: true + dev: true optional: true - '@rollup/rollup-win32-x64-msvc@4.17.2': - optional: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true - '@simple-dom/document@1.4.0': - dependencies: - '@simple-dom/interface': 1.4.0 + /assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} - '@simple-dom/interface@1.4.0': {} + /ast-types@0.13.3: + resolution: {integrity: sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==} + engines: {node: '>=4'} - '@simple-dom/parser@1.4.0': + /async-disk-cache@1.3.5: + resolution: {integrity: sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ==} dependencies: - '@simple-dom/interface': 1.4.0 + debug: 2.6.9 + heimdalljs: 0.2.6 + istextorbinary: 2.1.0 + mkdirp: 0.5.6 + rimraf: 2.7.1 + rsvp: 3.6.2 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color - '@simple-dom/serializer@1.4.0': + /async-disk-cache@2.1.0: + resolution: {integrity: sha512-iH+boep2xivfD9wMaZWkywYIURSmsL96d6MoqrC94BnGSvXE4Quf8hnJiHGFYhw/nLeIa1XyRaf4vvcvkwAefg==} + engines: {node: 8.* || >= 10.*} dependencies: - '@simple-dom/interface': 1.4.0 + debug: 4.3.7(supports-color@8.1.1) + heimdalljs: 0.2.6 + istextorbinary: 2.6.0 + mkdirp: 0.5.6 + rimraf: 3.0.2 + rsvp: 4.8.5 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color - '@simple-dom/void-map@1.4.0': {} + /async-promise-queue@1.0.5: + resolution: {integrity: sha512-xi0aQ1rrjPWYmqbwr18rrSKbSaXIeIwSd1J4KAgVfkq8utNbdZoht7GfvfY6swFUAMJ9obkc4WPJmtGwl+B8dw==} + dependencies: + async: 2.6.4 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color - '@sindresorhus/is@0.14.0': {} + /async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - '@socket.io/component-emitter@3.1.0': {} + /async@0.9.2: + resolution: {integrity: sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==} + requiresBuild: true + dev: true + optional: true - '@szmarczak/http-timer@1.1.2': + /async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} dependencies: - defer-to-connect: 1.1.3 - - '@tootallnate/once@1.1.2': {} + lodash: 4.17.21 - '@tootallnate/once@2.0.0': {} + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - '@types/babel__code-frame@7.0.3': {} + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} - '@types/body-parser@1.19.2': - dependencies: - '@types/connect': 3.4.35 - '@types/node': 18.15.10 + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true - '@types/broccoli-plugin@3.0.0': + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} dependencies: - broccoli-plugin: 4.0.7 - transitivePeerDependencies: - - supports-color + possible-typed-array-names: 1.0.0 - '@types/chai-as-promised@7.1.5': - dependencies: - '@types/chai': 4.3.4 + /aws-sign2@0.5.0: + resolution: {integrity: sha512-oqUX0DM5j7aPWPCnpWebiyNIj2wiNI87ZxnOMoGv0aE4TGlBy2N+5iWc6dQ/NOKZaBD2W6PVz8jtOGkWzSC5EA==} + requiresBuild: true + dev: true + optional: true - '@types/chai@4.3.4': {} + /babel-import-util@0.2.0: + resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} + engines: {node: '>= 12.*'} + dev: true - '@types/connect@3.4.35': - dependencies: - '@types/node': 18.15.10 + /babel-import-util@1.4.1: + resolution: {integrity: sha512-TNdiTQdPhXlx02pzG//UyVPSKE7SNWjY0n4So/ZnjQpWwaM5LvWBLkWa1JKll5u06HNscHD91XZPuwrMg1kadQ==} + engines: {node: '>= 12.*'} + dev: true - '@types/cookie@0.4.1': {} + /babel-import-util@2.1.1: + resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} + engines: {node: '>= 12.*'} - '@types/cors@2.8.13': - dependencies: - '@types/node': 18.15.10 + /babel-import-util@3.0.0: + resolution: {integrity: sha512-4YNPkuVsxAW5lnSTa6cn4Wk49RX6GAB6vX+M6LqEtN0YePqoFczv1/x0EyLK/o+4E1j9jEuYj5Su7IEPab5JHQ==} + engines: {node: '>= 12.*'} - '@types/ember-qunit@5.0.2(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /babel-loader@8.4.1(@babel/core@7.26.0)(webpack@5.94.0): + resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: 5.94.0 dependencies: - '@types/ember-resolver': 5.0.13(@babel/core@7.21.4) - '@types/ember__test': 4.0.1(@babel/core@7.21.4) - '@types/ember__test-helpers': 2.9.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@types/qunit': 2.19.4 - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - ember-source - - supports-color + '@babel/core': 7.26.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.94.0 - '@types/ember-resolver@5.0.13(@babel/core@7.21.4)': + /babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.94.0): + resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: 5.94.0 dependencies: - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - '@types/ember__owner': 4.0.3 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@types/ember-testing-helpers@0.0.4': - dependencies: - '@types/jquery': 3.5.16 - '@types/rsvp': 4.0.4 - - '@types/ember@4.0.3(@babel/core@7.21.4)': - dependencies: - '@types/ember__application': 4.0.5(@babel/core@7.21.4) - '@types/ember__array': 4.0.3(@babel/core@7.21.4) - '@types/ember__component': 4.0.12(@babel/core@7.21.4) - '@types/ember__controller': 4.0.4(@babel/core@7.21.4) - '@types/ember__debug': 4.0.3(@babel/core@7.21.4) - '@types/ember__engine': 4.0.4(@babel/core@7.21.4) - '@types/ember__error': 4.0.2 - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - '@types/ember__polyfills': 4.0.1 - '@types/ember__routing': 4.0.12(@babel/core@7.21.4) - '@types/ember__runloop': 4.0.2(@babel/core@7.21.4) - '@types/ember__service': 4.0.2(@babel/core@7.21.4) - '@types/ember__string': 3.0.15 - '@types/ember__template': 4.0.1 - '@types/ember__test': 4.0.1(@babel/core@7.21.4) - '@types/ember__utils': 4.0.2(@babel/core@7.21.4) - '@types/htmlbars-inline-precompile': 3.0.0 - '@types/rsvp': 4.0.4 - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@babel/core': 7.26.0(supports-color@8.1.1) + find-cache-dir: 4.0.0 + schema-utils: 4.2.0 + webpack: 5.94.0 + dev: true - '@types/ember__application@4.0.5(@babel/core@7.21.4)': + /babel-plugin-debug-macros@0.2.0(@babel/core@7.26.0): + resolution: {integrity: sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-beta.42 dependencies: - '@glimmer/component': 1.1.2(@babel/core@7.21.4) - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/ember__engine': 4.0.4(@babel/core@7.21.4) - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - '@types/ember__owner': 4.0.3 - '@types/ember__routing': 4.0.12(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@babel/core': 7.26.0 + semver: 5.7.2 - '@types/ember__array@4.0.3(@babel/core@7.21.4)': + /babel-plugin-debug-macros@0.3.4(@babel/core@7.26.0): + resolution: {integrity: sha512-wfel/vb3pXfwIDZUrkoDrn5FHmlWI96PCJ3UCDv2a86poJ3EQrnArNW5KfHSVJ9IOgxHbo748cQt7sDU+0KCEw==} + engines: {node: '>=6'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@babel/core': 7.26.0 + semver: 5.7.2 - '@types/ember__component@4.0.12(@babel/core@7.21.4)': + /babel-plugin-debug-macros@1.0.2(@babel/core@7.26.0): + resolution: {integrity: sha512-ADkMh1LL45678c+4iGn3Fp8hdI9qvxGBkH5x9HNiIlgYJGdQWmYNcA2cS3XAr76N85kDCg4VpqsTN1hFX2jbEA==} + engines: {node: '>=16'} + peerDependencies: + '@babel/core': ^7.0.0 dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@babel/core': 7.26.0 + babel-import-util: 2.1.1 + semver: 7.6.3 + dev: true - '@types/ember__controller@4.0.4': + /babel-plugin-ember-data-packages-polyfill@0.1.2: + resolution: {integrity: sha512-kTHnOwoOXfPXi00Z8yAgyD64+jdSXk3pknnS7NlqnCKAU6YDkXZ4Y7irl66kaZjZn0FBBt0P4YOZFZk85jYOww==} + engines: {node: 6.* || 8.* || 10.* || >= 12.*} dependencies: - '@types/ember__object': 4.0.5 + '@ember-data/rfc395-data': 0.0.4 - '@types/ember__controller@4.0.4(@babel/core@7.21.4)': + /babel-plugin-ember-modules-api-polyfill@3.5.0: + resolution: {integrity: sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + ember-rfc176-data: 0.3.18 - '@types/ember__debug@4.0.3(@babel/core@7.21.4)': + /babel-plugin-ember-template-compilation@2.3.0: + resolution: {integrity: sha512-4ZrKVSqdw5PxEKRbqfOpPhrrNBDG3mFPhyT6N1Oyyem81ZIkCvNo7TPKvlTHeFxqb6HtUvCACP/pzFpZ74J4pg==} + engines: {node: '>= 12.*'} dependencies: - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - '@types/ember__owner': 4.0.3 - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@glimmer/syntax': 0.84.3 + babel-import-util: 3.0.0 - '@types/ember__engine@4.0.4(@babel/core@7.21.4)': + /babel-plugin-htmlbars-inline-precompile@5.3.1: + resolution: {integrity: sha512-QWjjFgSKtSRIcsBhJmEwS2laIdrA6na8HAlc/pEAhjHgQsah/gMiBFRZvbQTy//hWxR4BMwV7/Mya7q5H8uHeA==} + engines: {node: 10.* || >= 12.*} dependencies: - '@types/ember__object': 4.0.5(@babel/core@7.21.4) - '@types/ember__owner': 4.0.3 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@types/ember__error@4.0.2': {} + babel-plugin-ember-modules-api-polyfill: 3.5.0 + line-column: 1.0.2 + magic-string: 0.25.9 + parse-static-imports: 1.1.0 + string.prototype.matchall: 4.0.11 - '@types/ember__object@4.0.5': + /babel-plugin-module-resolver@5.0.2: + resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==} dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/rsvp': 4.0.4 + find-babel-config: 2.1.2 + glob: 9.3.5 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.8 - '@types/ember__object@4.0.5(@babel/core@7.21.4)': + /babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/rsvp': 4.0.4 + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 transitivePeerDependencies: - - '@babel/core' - supports-color - '@types/ember__owner@4.0.3': {} - - '@types/ember__polyfills@4.0.1': {} - - '@types/ember__routing@4.0.12(@babel/core@7.21.4)': + /babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - '@types/ember__controller': 4.0.4 - '@types/ember__object': 4.0.5 - '@types/ember__service': 4.0.2 + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)(supports-color@8.1.1) + semver: 6.3.1 transitivePeerDependencies: - - '@babel/core' - supports-color + dev: true - '@types/ember__runloop@4.0.2(@babel/core@7.21.4)': + /babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 transitivePeerDependencies: - - '@babel/core' - supports-color - '@types/ember__service@4.0.2': - dependencies: - '@types/ember__object': 4.0.5 - - '@types/ember__service@4.0.2(@babel/core@7.21.4)': + /babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@types/ember__object': 4.0.5(@babel/core@7.21.4) + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)(supports-color@8.1.1) + core-js-compat: 3.39.0 transitivePeerDependencies: - - '@babel/core' - supports-color + dev: true - '@types/ember__string@3.0.10': {} - - '@types/ember__string@3.0.15': {} - - '@types/ember__template@4.0.1': {} - - '@types/ember__test-helpers@2.9.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))': + /babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@ember/test-helpers': 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - ember-source - supports-color - '@types/ember__test@4.0.1(@babel/core@7.21.4)': + /babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0)(supports-color@8.1.1): + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@types/ember__application': 4.0.5(@babel/core@7.21.4) + '@babel/core': 7.26.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)(supports-color@8.1.1) transitivePeerDependencies: - - '@babel/core' - supports-color + dev: true - '@types/ember__utils@4.0.2(@babel/core@7.21.4)': - dependencies: - '@types/ember': 4.0.3(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + /babel-plugin-syntax-dynamic-import@6.18.0: + resolution: {integrity: sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==} - '@types/eslint-scope@3.7.4': - dependencies: - '@types/eslint': 8.21.3 - '@types/estree': 1.0.0 + /babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + dev: true - '@types/eslint@8.21.3': + /backbone@1.6.0: + resolution: {integrity: sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==} dependencies: - '@types/estree': 1.0.0 - '@types/json-schema': 7.0.11 - - '@types/estree@0.0.51': {} - - '@types/estree@1.0.0': {} + underscore: 1.13.7 - '@types/estree@1.0.5': {} - - '@types/express-serve-static-core@4.17.33': - dependencies: - '@types/node': 18.15.10 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 + /backburner.js@2.8.0: + resolution: {integrity: sha512-zYXY0KvpD7/CWeOLF576mV8S+bQsaIoj/GNLXXB+Eb8SJcQy5lqSjkRrZ0MZhdKUs9QoqmGNIEIe3NQfGiiscQ==} - '@types/express@4.17.17': + /badge-maker@4.1.0: + resolution: {integrity: sha512-qYImXoz0WZRMaauqSMo6QNurKp26K3RcOhefuGfno50xmAzHEJsgHbP4gnHs6Ps53KgQgFi4MJKB6Rq8H7siww==} + engines: {node: '>=16'} + hasBin: true dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.33 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.1 + anafanafo: 2.0.0 + css-color-converter: 2.0.0 + dev: true - '@types/fs-extra@5.1.0': - dependencies: - '@types/node': 18.15.10 + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - '@types/fs-extra@8.1.2': - dependencies: - '@types/node': 18.15.10 + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - '@types/glob@7.2.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 18.15.10 + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} - '@types/glob@8.1.0': + /base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 18.15.10 - - '@types/htmlbars-inline-precompile@3.0.0': {} + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 - '@types/jquery@3.5.16': + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} dependencies: - '@types/sizzle': 2.3.3 - - '@types/json-schema@7.0.11': {} - - '@types/json5@0.0.29': {} + safe-buffer: 5.1.2 - '@types/keyv@3.1.4': + /better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} dependencies: - '@types/node': 18.15.10 + is-windows: 1.0.2 - '@types/mime@3.0.1': {} + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - '@types/minimatch@3.0.5': {} + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + requiresBuild: true - '@types/minimatch@5.1.2': {} + /binary-search@1.3.6: + resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==} + dev: true - '@types/node@18.15.10': {} + /binaryextensions@2.3.0: + resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==} + engines: {node: '>=0.8'} - '@types/qs@6.9.7': {} + /bind-decorator@1.0.11: + resolution: {integrity: sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==} + dev: true - '@types/qunit@2.19.4': {} + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 - '@types/range-parser@1.2.4': {} + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - '@types/resolve@1.20.2': {} + /blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + dev: true - '@types/responselike@1.0.0': + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dependencies: - '@types/node': 18.15.10 + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color - '@types/rimraf@2.0.5': + /body@5.1.0: + resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} dependencies: - '@types/glob': 8.1.0 - '@types/node': 18.15.10 - - '@types/rsvp@4.0.4': {} - - '@types/semver@7.3.13': {} + continuable-cache: 0.3.1 + error: 7.2.1 + raw-body: 1.1.7 + safe-json-parse: 1.0.1 - '@types/serve-static@1.15.1': + /bole@5.0.17: + resolution: {integrity: sha512-q6F82qEcUQTP178ZEY4WI1zdVzxy+fOnSF1dOMyC16u1fc0c24YrDPbgxA6N5wGHayCUdSBWsF8Oy7r2AKtQdA==} dependencies: - '@types/mime': 3.0.1 - '@types/node': 18.15.10 - - '@types/sizzle@2.3.3': {} + fast-safe-stringify: 2.1.1 + individual: 3.0.0 - '@types/source-map@0.5.7': + /boom@0.4.2: + resolution: {integrity: sha512-OvfN8y1oAxxphzkl2SnCS+ztV/uVKTATtgLjWYg/7KwcNyf3rzpHxNQJZCKtsZd4+MteKczhWbSjtEX4bGgU9g==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + requiresBuild: true dependencies: - source-map: 0.7.4 + hoek: 0.9.1 + dev: true + optional: true - '@types/ssri@7.1.5': + /boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} dependencies: - '@types/node': 18.15.10 - - '@types/supports-color@8.1.1': {} - - '@types/symlink-or-copy@1.2.0': {} - - '@types/tmp@0.0.33': {} - - '@types/yargs-parser@21.0.0': {} + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 - '@types/yargs@17.0.23': + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: - '@types/yargs-parser': 21.0.0 + balanced-match: 1.0.2 + concat-map: 0.0.1 - '@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint@8.37.0)(typescript@5.0.3)': + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: - '@eslint-community/regexpp': 4.4.1 - '@typescript-eslint/parser': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/type-utils': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - '@typescript-eslint/utils': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - debug: 4.3.4 - eslint: 8.37.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.3.8 - tsutils: 3.21.0(typescript@5.0.3) - optionalDependencies: - typescript: 5.0.3 - transitivePeerDependencies: - - supports-color + balanced-match: 1.0.2 - '@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3)': + /braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} dependencies: - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.0.3) - debug: 4.3.4 - eslint: 8.37.0 - optionalDependencies: - typescript: 5.0.3 + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@5.57.1': + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} dependencies: - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/visitor-keys': 5.57.1 + fill-range: 7.1.1 - '@typescript-eslint/type-utils@5.57.1(eslint@8.37.0)(typescript@5.0.3)': + /broccoli-babel-transpiler@8.0.0(@babel/core@7.26.0): + resolution: {integrity: sha512-3HEp3flvasUKJGWERcrPgM1SWvHJ0O/fmbEtY9L4kDyMSnqjY6hTYvNvgWCIgbwXAYAUlZP0vjAQsmyLNGLwFw==} + engines: {node: 16.* || >= 18} + peerDependencies: + '@babel/core': ^7.17.9 dependencies: - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.0.3) - '@typescript-eslint/utils': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - debug: 4.3.4 - eslint: 8.37.0 - tsutils: 3.21.0(typescript@5.0.3) - optionalDependencies: - typescript: 5.0.3 + '@babel/core': 7.26.0 + broccoli-persistent-filter: 3.1.3 + clone: 2.1.2 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.1.1 + rsvp: 4.8.5 + workerpool: 6.5.1 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@5.57.1': {} - - '@typescript-eslint/typescript-estree@5.57.1(typescript@5.0.3)': + /broccoli-builder@0.18.14: + resolution: {integrity: sha512-YoUHeKnPi4xIGZ2XDVN9oHNA9k3xF5f5vlA+1wvrxIIDXqQU97gp2FxVAF503Zxdtt0C5CRB5n+47k2hlkaBzA==} + engines: {node: '>= 0.10.0'} dependencies: - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/visitor-keys': 5.57.1 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.6.0 - tsutils: 3.21.0(typescript@5.0.3) - optionalDependencies: - typescript: 5.0.3 + broccoli-node-info: 1.1.0 + heimdalljs: 0.2.6 + promise-map-series: 0.2.3 + quick-temp: 0.1.8 + rimraf: 2.7.1 + rsvp: 3.6.2 + silent-error: 1.1.1 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.57.1(eslint@8.37.0)(typescript@5.0.3)': + /broccoli-caching-writer@2.3.1: + resolution: {integrity: sha512-lfoDx98VaU8tG4mUXCxKdKyw2Lr+iSIGUjCgV83KC2zRC07SzYTGuSsMqpXFiOQlOGuoJxG3NRoyniBa1BWOqA==} dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.37.0) - '@types/json-schema': 7.0.11 - '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.57.1 - '@typescript-eslint/types': 5.57.1 - '@typescript-eslint/typescript-estree': 5.57.1(typescript@5.0.3) - eslint: 8.37.0 - eslint-scope: 5.1.1 - semver: 7.6.0 + broccoli-kitchen-sink-helpers: 0.2.9 + broccoli-plugin: 1.1.0 + debug: 2.6.9 + rimraf: 2.7.1 + rsvp: 3.6.2 + walk-sync: 0.2.7 transitivePeerDependencies: - supports-color - - typescript + dev: true - '@typescript-eslint/visitor-keys@5.57.1': + /broccoli-caching-writer@3.0.3: + resolution: {integrity: sha512-g644Kb5uBPsy+6e2DvO3sOc+/cXZQQNgQt64QQzjA9TSdP0dl5qvetpoNIx4sy/XIjrPYG1smEidq9Z9r61INw==} dependencies: - '@typescript-eslint/types': 5.57.1 - eslint-visitor-keys: 3.4.0 + broccoli-kitchen-sink-helpers: 0.3.1 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + rimraf: 2.7.1 + rsvp: 3.6.2 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/ast@1.11.1': + /broccoli-clean-css@1.1.0: + resolution: {integrity: sha512-S7/RWWX+lL42aGc5+fXVLnwDdMtS0QEWUFalDp03gJ9Na7zj1rWa351N2HZ687E2crM9g+eDWXKzD17cbcTepg==} dependencies: - '@webassemblyjs/helper-numbers': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - - '@webassemblyjs/floating-point-hex-parser@1.11.1': {} - - '@webassemblyjs/helper-api-error@1.11.1': {} - - '@webassemblyjs/helper-buffer@1.11.1': {} + broccoli-persistent-filter: 1.4.6 + clean-css-promise: 0.1.1 + inline-source-map-comment: 1.0.5 + json-stable-stringify: 1.1.1 + transitivePeerDependencies: + - supports-color + dev: true - '@webassemblyjs/helper-numbers@1.11.1': + /broccoli-concat@4.2.5: + resolution: {integrity: sha512-dFB5ATPwOyV8S2I7a07HxCoutoq23oY//LhM6Mou86cWUTB174rND5aQLR7Fu8FjFFLxoTbkk7y0VPITJ1IQrw==} + engines: {node: 10.* || >= 12.*} dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.11.1': {} + broccoli-debug: 0.6.5 + broccoli-kitchen-sink-helpers: 0.3.1 + broccoli-plugin: 4.0.7 + ensure-posix-path: 1.1.1 + fast-sourcemap-concat: 2.1.1 + find-index: 1.1.1 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 + lodash.merge: 4.6.2 + lodash.omit: 4.5.0 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/helper-wasm-section@1.11.1': + /broccoli-config-loader@1.0.1: + resolution: {integrity: sha512-MDKYQ50rxhn+g17DYdfzfEM9DjTuSGu42Db37A8TQHQe8geYEcUZ4SQqZRgzdAI3aRQNlA1yBHJfOeGmOjhLIg==} dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 + broccoli-caching-writer: 3.0.3 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/ieee754@1.11.1': + /broccoli-config-replace@1.1.2: + resolution: {integrity: sha512-qLlEY3V7p3ZWJNRPdPgwIM77iau1qR03S9BupMMFngjzBr7S6RSzcg96HbCYXmW9gfTbjRm9FC4CQT81SBusZg==} dependencies: - '@xtuc/ieee754': 1.2.0 + broccoli-kitchen-sink-helpers: 0.3.1 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fs-extra: 0.24.0 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/leb128@1.11.1': + /broccoli-debug@0.6.5: + resolution: {integrity: sha512-RIVjHvNar9EMCLDW/FggxFRXqpjhncM/3qq87bn/y+/zR9tqEkHvTqbyOc4QnB97NO2m6342w4wGkemkaeOuWg==} dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.11.1': {} + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 + tree-sync: 1.4.0 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/wasm-edit@1.11.1': + /broccoli-file-creator@2.1.1: + resolution: {integrity: sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw==} + engines: {node: ^4.5 || 6.* || >= 7.*} dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/helper-wasm-section': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-opt': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - '@webassemblyjs/wast-printer': 1.11.1 + broccoli-plugin: 1.3.1 + mkdirp: 0.5.6 - '@webassemblyjs/wasm-gen@1.11.1': - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + /broccoli-funnel-reducer@1.0.0: + resolution: {integrity: sha512-SaOCEdh+wnt2jFUV2Qb32m7LXyElvFwW3NKNaEJyi5PGQNwxfqpkc0KI6AbQANKgdj/40U2UC0WuGThFwuEUaA==} - '@webassemblyjs/wasm-opt@1.11.1': + /broccoli-funnel@3.0.8: + resolution: {integrity: sha512-ng4eIhPYiXqMw6SyGoxPHR3YAwEd2lr9FgBI1CyTbspl4txZovOsmzFkMkGAlu88xyvYXJqHiM2crfLa65T1BQ==} + engines: {node: 10.* || >= 12.*} dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 + array-equal: 1.0.2 + broccoli-plugin: 4.0.7 + debug: 4.3.7(supports-color@8.1.1) + fs-tree-diff: 2.0.1 + heimdalljs: 0.2.6 + minimatch: 3.1.2 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color - '@webassemblyjs/wasm-parser@1.11.1': + /broccoli-kitchen-sink-helpers@0.2.9: + resolution: {integrity: sha512-C+oEqivDofZv/h80rgN4WJkbZkbfwkrIeu8vFn4bb4m4jPd3ICNNplhkXGl3ps439pzc2yjZ1qIwz0yy8uHcQg==} dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + glob: 5.0.15 + mkdirp: 0.5.6 + dev: true - '@webassemblyjs/wast-printer@1.11.1': + /broccoli-kitchen-sink-helpers@0.3.1: + resolution: {integrity: sha512-gqYnKSJxBSjj/uJqeuRAzYVbmjWhG0mOZ8jrp6+fnUIOgLN6MvI7XxBECDHkYMIFPJ8Smf4xaI066Q2FqQDnXg==} dependencies: - '@webassemblyjs/ast': 1.11.1 - '@xtuc/long': 4.2.2 - - '@xmldom/xmldom@0.8.6': {} - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} + glob: 5.0.15 + mkdirp: 0.5.6 - '@zkochan/which@2.0.3': + /broccoli-merge-trees@4.2.0: + resolution: {integrity: sha512-nTrQe5AQtCrW4enLRvbD/vTLHqyW2tz+vsLXQe4IEaUhepuMGVKJJr+I8n34Vu6fPjmPLwTjzNC8izMIDMtHPw==} + engines: {node: 10.* || >= 12.*} dependencies: - isexe: 2.0.0 - - abab@2.0.6: {} - - abbrev@1.1.1: {} + broccoli-plugin: 4.0.7 + merge-trees: 2.0.0 + transitivePeerDependencies: + - supports-color - accepts@1.3.8: + /broccoli-middleware@2.1.1: + resolution: {integrity: sha512-BK8aPhQpOLsHWiftrqXQr84XsvzUqeaN4PlCQOYg5yM0M+WKAHtX2WFXmicSQZOVgKDyh5aeoNTFkHjBAEBzwQ==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: + ansi-html: 0.0.7 + handlebars: 4.7.8 + has-ansi: 3.0.0 mime-types: 2.1.35 - negotiator: 0.6.3 - - acorn-globals@6.0.0: - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - - acorn-import-assertions@1.8.0(acorn@8.8.2): - dependencies: - acorn: 8.8.2 - - acorn-jsx@5.3.2(acorn@8.8.2): - dependencies: - acorn: 8.8.2 - acorn-walk@7.2.0: {} + /broccoli-node-api@1.7.0: + resolution: {integrity: sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==} - acorn@7.4.1: {} + /broccoli-node-info@1.1.0: + resolution: {integrity: sha512-DUohSZCdfXli/3iN6SmxPbck1OVG8xCkrLx47R25his06xVc1ZmmrOsrThiM8BsCWirwyocODiYJqNP5W2Hg1A==} + engines: {node: '>= 0.10.0'} - acorn@8.8.2: {} + /broccoli-node-info@2.2.0: + resolution: {integrity: sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==} + engines: {node: 8.* || >= 10.*} - agent-base@6.0.2: + /broccoli-output-wrapper@3.2.5: + resolution: {integrity: sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==} + engines: {node: 10.* || >= 12.*} dependencies: - debug: 4.3.4 + fs-extra: 8.1.0 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 transitivePeerDependencies: - supports-color - agent-base@6.0.2(supports-color@8.1.1): + /broccoli-persistent-filter@1.4.6: + resolution: {integrity: sha512-0RejLwoC95kv4kta8KAa+FmECJCK78Qgm8SRDEK7YyU0N9Cx6KpY3UCDy9WELl3mCXLN8TokNxc7/hp3lL4lfw==} dependencies: - debug: 4.3.4(supports-color@8.1.1) + async-disk-cache: 1.3.5 + async-promise-queue: 1.0.5 + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rimraf: 2.7.1 + rsvp: 3.6.2 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 transitivePeerDependencies: - supports-color + dev: true - agentkeepalive@4.3.0: + /broccoli-persistent-filter@2.3.1: + resolution: {integrity: sha512-hVsmIgCDrl2NFM+3Gs4Cr2TA6UPaIZip99hN8mtkaUPgM8UeVnCbxelCvBjUBHo0oaaqP5jzqqnRVvb568Yu5g==} + engines: {node: 6.* || >= 8.*} dependencies: - debug: 4.3.4 - depd: 2.0.0 - humanize-ms: 1.2.1 + async-disk-cache: 1.3.5 + async-promise-queue: 1.0.5 + broccoli-plugin: 1.3.1 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rimraf: 2.7.1 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + sync-disk-cache: 1.3.4 + walk-sync: 1.1.4 transitivePeerDependencies: - supports-color - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - - ajv-formats@2.1.1: + /broccoli-persistent-filter@3.1.3: + resolution: {integrity: sha512-Q+8iezprZzL9voaBsDY3rQVl7c7H5h+bvv8SpzCZXPZgfBFCbx7KFQ2c3rZR6lW5k4Kwoqt7jG+rZMUg67Gwxw==} + engines: {node: 10.* || >= 12.*} dependencies: - ajv: 8.12.0 + async-disk-cache: 2.1.0 + async-promise-queue: 1.0.5 + broccoli-plugin: 4.0.7 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.1 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + promise-map-series: 0.2.3 + rimraf: 3.0.2 + symlink-or-copy: 1.3.1 + sync-disk-cache: 2.1.0 + transitivePeerDependencies: + - supports-color - ajv-keywords@3.5.2(ajv@6.12.6): + /broccoli-plugin@1.1.0: + resolution: {integrity: sha512-dY1QsA20of9wWEto8yhN7JQjpfjySmgeIMsvnQ9aBAv1wEJJCe04B0ekdgq7Bduyx9yWXdoC5CngGy81swmp2w==} dependencies: - ajv: 6.12.6 + promise-map-series: 0.2.3 + quick-temp: 0.1.8 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + dev: true - ajv-keywords@5.1.0(ajv@8.12.0): + /broccoli-plugin@1.3.1: + resolution: {integrity: sha512-DW8XASZkmorp+q7J4EeDEZz+LoyKLAd2XZULXyD9l4m9/hAKV3vjHmB1kiUshcWAYMgTP1m2i4NnqCE/23h6AQ==} dependencies: - ajv: 8.12.0 - fast-deep-equal: 3.1.3 + promise-map-series: 0.2.3 + quick-temp: 0.1.8 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 - ajv@6.12.6: + /broccoli-plugin@2.1.0: + resolution: {integrity: sha512-ElE4caljW4slapyEhSD9jU9Uayc8SoSABWdmY9SqbV8DHNxU6xg1jJsPcMm+cXOvggR3+G+OXAYQeFjWVnznaw==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 + promise-map-series: 0.2.3 + quick-temp: 0.1.8 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 - ajv@8.12.0: + /broccoli-plugin@4.0.7: + resolution: {integrity: sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==} + engines: {node: 10.* || >= 12.*} dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 + broccoli-node-api: 1.7.0 + broccoli-output-wrapper: 3.2.5 + fs-merger: 3.2.1 + promise-map-series: 0.3.0 + quick-temp: 0.1.8 + rimraf: 3.0.2 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color - amd-name-resolver@1.3.1: + /broccoli-slow-trees@3.1.0: + resolution: {integrity: sha512-FRI7mRTk2wjIDrdNJd6znS7Kmmne4VkAkl8Ix1R/VoePFMD0g0tEl671xswzFqaRjpT9Qu+CC4hdXDLDJBuzMw==} dependencies: - ensure-posix-path: 1.1.1 - object-hash: 1.3.1 + heimdalljs: 0.2.6 - amdefine@1.0.1: {} + /broccoli-source@1.1.0: + resolution: {integrity: sha512-ahvqmwF6Yvh6l+sTJJdey4o4ynwSH8swSSBSGmUXGSPPCqBWvquWB/4rWN65ZArKilBFq/29O0yQnZNIf//sTg==} + dev: true - ansi-align@3.0.1: + /broccoli-source@3.0.1: + resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} + engines: {node: 8.* || 10.* || >= 12.*} dependencies: - string-width: 4.2.3 - - ansi-colors@4.1.1: {} + broccoli-node-api: 1.7.0 - ansi-diff@1.1.1: + /broccoli-sri-hash@2.1.2: + resolution: {integrity: sha512-toLD/v7ut2ajcH8JsdCMG2Bpq2qkwTcKM6CMzVMSAJjaz/KpK69fR+gSqe1dsjh+QTdxG0yVvkq3Sij/XMzV6A==} dependencies: - ansi-split: 1.0.1 - - ansi-escapes@3.2.0: {} + broccoli-caching-writer: 2.3.1 + mkdirp: 0.5.6 + rsvp: 3.6.2 + sri-toolbox: 0.2.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + dev: true - ansi-escapes@4.3.2: + /broccoli-stew@1.6.0: + resolution: {integrity: sha512-sUwCJNnYH4Na690By5xcEMAZqKgquUQnMAEuIiL3Z2k63mSw9Xg+7Ew4wCrFrMmXMcLpWjZDOm6Yqnq268N+ZQ==} + engines: {node: ^4.5 || 6.* || >= 7.*} dependencies: - type-fest: 0.21.3 - - ansi-html@0.0.7: {} - - ansi-regex@2.1.1: {} - - ansi-regex@3.0.1: {} - - ansi-regex@4.1.1: {} - - ansi-regex@5.0.1: {} + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-persistent-filter: 1.4.6 + broccoli-plugin: 1.3.1 + chalk: 2.4.2 + debug: 3.2.7 + ensure-posix-path: 1.1.1 + fs-extra: 5.0.0 + minimatch: 3.1.2 + resolve: 1.22.8 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + dev: true - ansi-split@1.0.1: + /broccoli-stew@3.0.0: + resolution: {integrity: sha512-NXfi+Vas24n3Ivo21GvENTI55qxKu7OwKRnCLWXld8MiLiQKQlWIq28eoARaFj0lTUFwUa4jKZeA7fW9PiWQeg==} + engines: {node: 8.* || >= 10.*} dependencies: - ansi-regex: 3.0.1 - - ansi-styles@2.2.1: {} + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 2.1.0 + chalk: 2.4.2 + debug: 4.3.7(supports-color@8.1.1) + ensure-posix-path: 1.1.1 + fs-extra: 8.1.0 + minimatch: 3.1.2 + resolve: 1.22.8 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color - ansi-styles@3.2.1: + /broccoli-string-replace@0.1.2: + resolution: {integrity: sha512-QHESTrrrPlKuXQNWsvXawSQbV2g34wCZ5oKgd6bntdOuN8VHxbg1BCBHqVY5HxXJhWelimgGxj3vI7ECkyij8g==} dependencies: - color-convert: 1.9.3 + broccoli-persistent-filter: 1.4.6 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true - ansi-styles@4.3.0: + /broccoli-terser-sourcemap@4.1.1: + resolution: {integrity: sha512-8sbpRf0/+XeszBJQM7vph2UNj4Kal0lCI/yubcrBIzb2NvYj5gjTHJABXOdxx5mKNmlCMu2hx2kvOtMpQsxrfg==} + engines: {node: ^10.12.0 || 12.* || >= 14} dependencies: - color-convert: 2.0.1 + async-promise-queue: 1.0.5 + broccoli-plugin: 4.0.7 + convert-source-map: 2.0.0 + debug: 4.3.7(supports-color@8.1.1) + lodash.defaultsdeep: 4.6.1 + matcher-collection: 2.0.1 + symlink-or-copy: 1.3.1 + terser: 5.36.0 + walk-sync: 2.2.0 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + dev: true - ansi-to-html@0.6.15: + /broccoli-test-helper@2.0.0: + resolution: {integrity: sha512-TKwh8dBT+RcxKEG+vAoaRRhZsCMwZIHPZbCzBNCA0nUi1aoFB/LVosqwMC6H9Ipe06FxY5hpQxDLFbnBMdUPsA==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - entities: 2.2.0 - - ansicolors@0.2.1: {} + '@types/tmp': 0.0.33 + broccoli: 2.3.0 + fixturify: 0.3.4 + fs-tree-diff: 0.5.9 + tmp: 0.0.33 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + dev: true - any-promise@1.3.0: {} + /broccoli-uglify-sourcemap@4.0.0: + resolution: {integrity: sha512-46yB4gw1Q3ALtBROY5QfKXNXxYK5uPSvER1OGjjh2t3piaipqBfuRXTzQZvmZ+Odr6/McY+J8XmxON4+lE1ukg==} + engines: {node: ^10.12.0 || 12.* || >= 14} + dependencies: + async-promise-queue: 1.0.5 + broccoli-plugin: 4.0.7 + debug: 4.3.7(supports-color@8.1.1) + lodash.defaultsdeep: 4.6.1 + matcher-collection: 2.0.1 + source-map-url: 0.4.1 + symlink-or-copy: 1.3.1 + terser: 5.36.0 + walk-sync: 2.2.0 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + dev: true - anymatch@2.0.0: + /broccoli@2.3.0: + resolution: {integrity: sha512-TeYMYlCGFK8EGk4Wce1G1uU3i52+YxRqP3WPOVDojC1zUk+Gi40wHBzUT2fncQZDl26dmCQMNugtHKjvUpcGQg==} + engines: {node: '>= 6'} dependencies: - micromatch: 3.1.10 - normalize-path: 2.1.1 + broccoli-node-info: 1.1.0 + broccoli-slow-trees: 3.1.0 + broccoli-source: 1.1.0 + commander: 2.20.3 + connect: 3.7.0 + esm: 3.2.25 + findup-sync: 2.0.0 + handlebars: 4.7.8 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mime-types: 2.1.35 + promise.prototype.finally: 3.1.8 + resolve-path: 1.4.0 + rimraf: 2.7.1 + sane: 4.1.0 + tmp: 0.0.33 + tree-sync: 1.4.0 + underscore.string: 3.3.6 + watch-detector: 0.1.0 transitivePeerDependencies: - supports-color + dev: true - anymatch@3.1.3: + /broccoli@3.5.2: + resolution: {integrity: sha512-sWi3b3fTUSVPDsz5KsQ5eCQNVAtLgkIE/HYFkEZXR/07clqmd4E/gFiuwSaqa9b+QTXc1Uemfb7TVWbEIURWDg==} + engines: {node: 8.* || >= 10.*} dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 + '@types/chai': 4.3.20 + '@types/chai-as-promised': 7.1.8 + '@types/express': 4.17.21 + ansi-html: 0.0.7 + broccoli-node-info: 2.2.0 + broccoli-slow-trees: 3.1.0 + broccoli-source: 3.0.1 + commander: 4.1.1 + connect: 3.7.0 + console-ui: 3.1.2 + esm: 3.2.25 + findup-sync: 4.0.0 + handlebars: 4.7.8 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + https: 1.0.0 + mime-types: 2.1.35 + resolve-path: 1.4.0 + rimraf: 3.0.2 + sane: 4.1.0 + tmp: 0.0.33 + tree-sync: 2.1.0 + underscore.string: 3.3.6 + watch-detector: 1.0.2 + transitivePeerDependencies: + - supports-color - aproba@2.0.0: {} + /browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + dev: true - archy@1.0.0: {} + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true - are-we-there-yet@3.0.1: + /browserslist-to-esbuild@2.1.1(browserslist@4.24.2): + resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + browserslist: '*' dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 + browserslist: 4.24.2 + meow: 13.2.0 + dev: true - argparse@1.0.10: + /browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true dependencies: - sprintf-js: 1.0.3 + caniuse-lite: 1.0.30001680 + electron-to-chromium: 1.5.57 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) - argparse@2.0.1: {} - - arr-diff@4.0.0: {} - - arr-flatten@1.1.0: {} - - arr-union@3.1.0: {} - - array-back@3.1.0: {} - - array-buffer-byte-length@1.0.0: + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 - - array-equal@1.0.0: {} + node-int64: 0.4.0 - array-flatten@1.1.1: {} + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - array-includes@3.1.6: + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - get-intrinsic: 1.2.0 - is-string: 1.0.7 + base64-js: 1.5.1 + ieee754: 1.2.1 - array-to-error@1.1.1: + /bun-types@1.1.34: + resolution: {integrity: sha512-br5QygTEL/TwB4uQOb96Ky22j4Gq2WxWH/8Oqv20fk5HagwKXo/akB+LiYgSfzexCt6kkcUaVm+bKiPl71xPvw==} dependencies: - array-to-sentence: 1.1.0 + '@types/node': 20.12.14 + '@types/ws': 8.5.13 + dev: true - array-to-sentence@1.1.0: {} - - array-union@2.1.0: {} + /bytes@1.0.0: + resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} - array-unique@0.3.2: {} + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} - array.prototype.flat@1.3.1: + /cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - es-shim-unscopables: 1.0.0 + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + dev: true - array.prototype.flatmap@1.3.1: + /cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - es-shim-unscopables: 1.0.0 + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 - as-table@1.0.55: + /cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} dependencies: - printable-characters: 1.0.42 - - asn1@0.1.11: - optional: true - - assert-never@1.2.1: {} - - assert-plus@0.1.5: - optional: true - - assertion-error@1.1.0: {} - - assign-symbols@1.0.0: {} - - ast-types@0.13.3: {} + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + dev: true - async-disk-cache@1.3.5: + /calculate-cache-key-for-tree@2.0.0: + resolution: {integrity: sha512-Quw8a6y8CPmRd6eU+mwypktYCwUcf8yVFIRbNZ6tPQEckX9yd+EBVEPC/GSZZrMWH9e7Vz4pT7XhpmyApRByLQ==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - debug: 2.6.9 - heimdalljs: 0.2.6 - istextorbinary: 2.1.0 - mkdirp: 0.5.6 - rimraf: 2.7.1 - rsvp: 3.6.2 - username-sync: 1.0.3 - transitivePeerDependencies: - - supports-color + json-stable-stringify: 1.1.1 - async-disk-cache@2.1.0: + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} dependencies: - debug: 4.3.4 - heimdalljs: 0.2.6 - istextorbinary: 2.6.0 - mkdirp: 0.5.6 - rimraf: 3.0.2 - rsvp: 4.8.5 - username-sync: 1.0.3 - transitivePeerDependencies: - - supports-color + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} - async-promise-queue@1.0.5: + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} dependencies: - async: 2.6.4 - debug: 2.6.9 - transitivePeerDependencies: - - supports-color + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 - async@0.2.10: {} + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} - async@0.9.2: - optional: true + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} - async@2.6.4: + /can-symlink@1.0.0: + resolution: {integrity: sha512-RbsNrFyhwkx+6psk/0fK/Q9orOUr9VMxohGd8vTa4djf4TGLfblBgUfqZChrZuW0Q+mz2eBPFLusw9Jfukzmhg==} + hasBin: true dependencies: - lodash: 4.17.21 - - asynckit@0.4.0: {} + tmp: 0.0.28 - at-least-node@1.0.0: {} + /can-write-to-dir@1.1.1: + resolution: {integrity: sha512-eOgiEWqjppB+3DN/5E82EQ8dTINus8d9GXMCbEsUnp2hcUIcXmBvzWmD3tXMk3CuBK0v+ddK9qw0EAF+JVRMjQ==} + engines: {node: '>=10.13'} + dependencies: + path-temp: 2.1.0 - atob@2.1.2: {} + /caniuse-lite@1.0.30001680: + resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} - available-typed-arrays@1.0.5: {} + /capture-exit@2.0.0: + resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} + engines: {node: 6.* || 8.* || >= 10.*} + dependencies: + rsvp: 4.8.5 - aws-sign2@0.5.0: - optional: true + /cardinal@1.0.0: + resolution: {integrity: sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg==} + hasBin: true + dependencies: + ansicolors: 0.2.1 + redeyed: 1.0.1 - babel-code-frame@6.26.0: + /chai-as-promised@6.0.0(chai@3.5.0): + resolution: {integrity: sha512-Zf5Dq6p4d0pApi662BtRe95oKYbEyNb+TLbIdwVSlewYxVhtMYwrTD3TAmcaf1XanuBw7egusnLxLXlMnv0myw==} + peerDependencies: + chai: '>= 2.1.2 < 4' dependencies: - chalk: 1.1.3 - esutils: 2.0.3 - js-tokens: 3.0.2 + chai: 3.5.0 + check-error: 1.0.3 + dev: true - babel-helper-builder-binary-assignment-operator-visitor@6.24.1(supports-color@8.1.1): + /chai-as-promised@7.1.2(chai@4.5.0): + resolution: {integrity: sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==} + peerDependencies: + chai: '>= 2.1.2 < 6' dependencies: - babel-helper-explode-assignable-expression: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + chai: 4.5.0 + check-error: 1.0.3 + dev: true - babel-helper-call-delegate@6.24.1(supports-color@8.1.1): + /chai-files@1.4.0: + resolution: {integrity: sha512-tPTx7H2kpR+wILWHRx8RxpXcRUdc2uH8su505C9R3p5GA+eYbZBXuxWC0RZbyElYi7X7Fp/V/S2PQjkakrT1mQ==} dependencies: - babel-helper-hoist-variables: 6.24.1 - babel-runtime: 6.26.0 - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + assertion-error: 1.1.0 + dev: true - babel-helper-define-map@6.26.0(supports-color@8.1.1): + /chai@3.5.0: + resolution: {integrity: sha512-eRYY0vPS2a9zt5w5Z0aCeWbrXTEyvk7u/Xf71EzNObrjSCPgMm1Nku/D/u2tiqHBX5j40wWhj54YJLtgn8g55A==} + engines: {node: '>= 0.4.0'} dependencies: - babel-helper-function-name: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-types: 6.26.0 - lodash: 4.17.21 - transitivePeerDependencies: - - supports-color + assertion-error: 1.1.0 + deep-eql: 0.1.3 + type-detect: 1.0.0 + dev: true - babel-helper-explode-assignable-expression@6.24.1(supports-color@8.1.1): + /chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} dependencies: - babel-runtime: 6.26.0 - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + dev: true - babel-helper-function-name@6.24.1(supports-color@8.1.1): + /chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} dependencies: - babel-helper-get-function-arity: 6.24.1 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + dev: true - babel-helper-get-function-arity@6.24.1: + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 - babel-helper-hoist-variables@6.24.1: + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - babel-helper-optimise-call-expression@6.24.1: + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + /char-width-table-consumer@1.0.0: + resolution: {integrity: sha512-Fz4UD0LBpxPgL9i29CJ5O4KANwaMnX/OhhbxzvNa332h+9+nRKyeuLw4wA51lt/ex67+/AdsoBQJF3kgX2feYQ==} dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + binary-search: 1.3.6 + dev: true + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - babel-helper-regex@6.26.0: + /charm@1.0.2: + resolution: {integrity: sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw==} dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 - lodash: 4.17.21 + inherits: 2.0.4 - babel-helper-remap-async-to-generator@6.24.1(supports-color@8.1.1): + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: - babel-helper-function-name: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + get-func-name: 2.0.2 + dev: true - babel-helper-replace-supers@6.24.1(supports-color@8.1.1): + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} dependencies: - babel-helper-optimise-call-expression: 6.24.1 - babel-messages: 6.23.0 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 - babel-import-util@0.2.0: {} + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: true - babel-import-util@1.3.0: {} + /chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} - babel-import-util@2.1.1: {} + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} - babel-loader@8.3.0(@babel/core@7.21.4)(webpack@5.77.0): - dependencies: - '@babel/core': 7.21.4 - find-cache-dir: 3.3.2 - loader-utils: 2.0.4 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.77.0 + /ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + dev: true - babel-loader@8.3.0(@babel/core@7.24.5(supports-color@8.1.1))(webpack@5.77.0): + /class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} dependencies: - '@babel/core': 7.24.5(supports-color@8.1.1) - find-cache-dir: 3.3.2 - loader-utils: 2.0.4 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.77.0 + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 - babel-messages@6.23.0: - dependencies: - babel-runtime: 6.26.0 + /clean-base-url@1.0.0: + resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} - babel-plugin-check-es2015-constants@6.22.0: + /clean-css-promise@0.1.1: + resolution: {integrity: sha512-tzWkANXMD70ETa/wAu2TXAAxYWS0ZjVUFM2dVik8RQBoAbGMFJv4iVluz3RpcoEbo++fX4RV/BXfgGoOjp8o3Q==} dependencies: - babel-runtime: 6.26.0 + array-to-error: 1.1.1 + clean-css: 3.4.28 + pinkie-promise: 2.0.1 + dev: true - babel-plugin-debug-macros@0.2.0(@babel/core@7.21.4): + /clean-css@3.4.28: + resolution: {integrity: sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==} + engines: {node: '>=0.10.0'} + hasBin: true dependencies: - '@babel/core': 7.21.4 - semver: 5.7.1 + commander: 2.8.1 + source-map: 0.4.4 + dev: true - babel-plugin-debug-macros@0.2.0(@babel/core@7.24.5): - dependencies: - '@babel/core': 7.24.5 - semver: 5.7.1 + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} - babel-plugin-debug-macros@0.3.4(@babel/core@7.21.4): - dependencies: - '@babel/core': 7.21.4 - semver: 5.7.1 + /clean-up-path@1.0.0: + resolution: {integrity: sha512-PHGlEF0Z6976qQyN6gM7kKH6EH0RdfZcc8V+QhFe36eRxV0SMH5OUBZG7Bxa9YcreNzyNbK63cGiZxdSZgosRw==} - babel-plugin-debug-macros@0.3.4(@babel/core@7.24.5): - dependencies: - '@babel/core': 7.24.5 - semver: 5.7.1 + /cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} - babel-plugin-ember-data-packages-polyfill@0.1.2: + /cli-columns@4.0.0: + resolution: {integrity: sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==} + engines: {node: '>= 10'} dependencies: - '@ember-data/rfc395-data': 0.0.4 + string-width: 4.2.3 + strip-ansi: 6.0.1 - babel-plugin-ember-modules-api-polyfill@3.5.0: + /cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} dependencies: - ember-rfc176-data: 0.3.18 + restore-cursor: 2.0.0 - babel-plugin-ember-template-compilation@2.0.0: + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} dependencies: - babel-import-util: 1.3.0 + restore-cursor: 3.1.0 - babel-plugin-ember-template-compilation@2.2.2: + /cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true dependencies: - '@glimmer/syntax': 0.84.3 - babel-import-util: 2.1.1 + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + dev: true - babel-plugin-filter-imports@4.0.0: - dependencies: - '@babel/types': 7.21.3 - lodash: 4.17.21 + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} - babel-plugin-htmlbars-inline-precompile@5.3.1: + /cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} dependencies: - babel-plugin-ember-modules-api-polyfill: 3.5.0 - line-column: 1.0.2 - magic-string: 0.25.9 - parse-static-imports: 1.1.0 - string.prototype.matchall: 4.0.8 + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true - babel-plugin-module-resolver@3.2.0: + /cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} dependencies: - find-babel-config: 1.2.0 - glob: 7.2.3 - pkg-up: 2.0.0 - reselect: 3.0.1 - resolve: 1.22.1 + colors: 1.0.3 - babel-plugin-module-resolver@4.1.0: - dependencies: - find-babel-config: 1.2.0 - glob: 7.2.3 - pkg-up: 3.1.0 - reselect: 4.1.7 - resolve: 1.22.1 + /cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} - babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.21.4): - dependencies: - '@babel/compat-data': 7.21.4 - '@babel/core': 7.21.4 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.4) - semver: 6.3.0 - transitivePeerDependencies: - - supports-color + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} - babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.5): + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - core-js-compat: 3.37.0 - transitivePeerDependencies: - - supports-color + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true - babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.4): + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.4) - core-js-compat: 3.29.1 - transitivePeerDependencies: - - supports-color + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 - babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.4): + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} dependencies: - '@babel/core': 7.21.4 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.4) - transitivePeerDependencies: - - supports-color + mimic-response: 1.0.1 + dev: true - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.5): - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - transitivePeerDependencies: - - supports-color + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} - babel-plugin-syntax-async-functions@6.13.0: {} + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} - babel-plugin-syntax-dynamic-import@6.18.0: {} + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true - babel-plugin-syntax-exponentiation-operator@6.13.0: {} + /collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 - babel-plugin-syntax-trailing-function-commas@6.22.0: {} + /color-convert@0.5.3: + resolution: {integrity: sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==} + dev: true - babel-plugin-transform-async-to-generator@6.24.1(supports-color@8.1.1): + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: - babel-helper-remap-async-to-generator: 6.24.1(supports-color@8.1.1) - babel-plugin-syntax-async-functions: 6.13.0 - babel-runtime: 6.26.0 - transitivePeerDependencies: - - supports-color + color-name: 1.1.3 - babel-plugin-transform-es2015-arrow-functions@6.22.0: + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} dependencies: - babel-runtime: 6.26.0 + color-name: 1.1.4 - babel-plugin-transform-es2015-block-scoped-functions@6.22.0: - dependencies: - babel-runtime: 6.26.0 + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - babel-plugin-transform-es2015-block-scoping@6.26.0(supports-color@8.1.1): - dependencies: - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - lodash: 4.17.21 - transitivePeerDependencies: - - supports-color + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - babel-plugin-transform-es2015-classes@6.24.1(supports-color@8.1.1): - dependencies: - babel-helper-define-map: 6.26.0(supports-color@8.1.1) - babel-helper-function-name: 6.24.1(supports-color@8.1.1) - babel-helper-optimise-call-expression: 6.24.1 - babel-helper-replace-supers: 6.24.1(supports-color@8.1.1) - babel-messages: 6.23.0 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true - babel-plugin-transform-es2015-computed-properties@6.24.1(supports-color@8.1.1): - dependencies: - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + /colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} - babel-plugin-transform-es2015-destructuring@6.23.0: - dependencies: - babel-runtime: 6.26.0 + /colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: true - babel-plugin-transform-es2015-duplicate-keys@6.24.1: + /combined-stream@0.0.7: + resolution: {integrity: sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==} + engines: {node: '>= 0.8'} + requiresBuild: true dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + delayed-stream: 0.0.5 + dev: true + optional: true - babel-plugin-transform-es2015-for-of@6.23.0: + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} dependencies: - babel-runtime: 6.26.0 + delayed-stream: 1.0.0 - babel-plugin-transform-es2015-function-name@6.24.1(supports-color@8.1.1): + /command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} dependencies: - babel-helper-function-name: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - babel-plugin-transform-es2015-literals@6.22.0: + /commander@2.8.1: + resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} + engines: {node: '>= 0.6.x'} dependencies: - babel-runtime: 6.26.0 + graceful-readlink: 1.0.1 + dev: true - babel-plugin-transform-es2015-modules-amd@6.24.1(supports-color@8.1.1): - dependencies: - babel-plugin-transform-es2015-modules-commonjs: 6.26.2(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} - babel-plugin-transform-es2015-modules-commonjs@6.26.2(supports-color@8.1.1): - dependencies: - babel-plugin-transform-strict-mode: 6.24.1 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + dev: false - babel-plugin-transform-es2015-modules-systemjs@6.24.1(supports-color@8.1.1): - dependencies: - babel-helper-hoist-variables: 6.24.1 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} - babel-plugin-transform-es2015-modules-umd@6.24.1(supports-color@8.1.1): - dependencies: - babel-plugin-transform-es2015-modules-amd: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true - babel-plugin-transform-es2015-object-super@6.24.1(supports-color@8.1.1): - dependencies: - babel-helper-replace-supers: 6.24.1(supports-color@8.1.1) - babel-runtime: 6.26.0 - transitivePeerDependencies: - - supports-color + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: false + optional: true - babel-plugin-transform-es2015-parameters@6.24.1(supports-color@8.1.1): + /comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} dependencies: - babel-helper-call-delegate: 6.24.1(supports-color@8.1.1) - babel-helper-get-function-arity: 6.24.1 - babel-runtime: 6.26.0 - babel-template: 6.26.0(supports-color@8.1.1) - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - transitivePeerDependencies: - - supports-color + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true - babel-plugin-transform-es2015-shorthand-properties@6.24.1: - dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + /common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} - babel-plugin-transform-es2015-spread@6.22.0: - dependencies: - babel-runtime: 6.26.0 + /common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + dev: true - babel-plugin-transform-es2015-sticky-regex@6.24.1: - dependencies: - babel-helper-regex: 6.26.0 - babel-runtime: 6.26.0 - babel-types: 6.26.0 + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true - babel-plugin-transform-es2015-template-literals@6.22.0: - dependencies: - babel-runtime: 6.26.0 + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - babel-plugin-transform-es2015-typeof-symbol@6.23.0: - dependencies: - babel-runtime: 6.26.0 + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - babel-plugin-transform-es2015-unicode-regex@6.24.1: + /compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} dependencies: - babel-helper-regex: 6.26.0 - babel-runtime: 6.26.0 - regexpu-core: 2.0.0 + mime-db: 1.53.0 - babel-plugin-transform-exponentiation-operator@6.24.1(supports-color@8.1.1): + /compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} + engines: {node: '>= 0.8.0'} dependencies: - babel-helper-builder-binary-assignment-operator-visitor: 6.24.1(supports-color@8.1.1) - babel-plugin-syntax-exponentiation-operator: 6.13.0 - babel-runtime: 6.26.0 + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 transitivePeerDependencies: - supports-color - babel-plugin-transform-regenerator@6.26.0: - dependencies: - regenerator-transform: 0.10.1 + /computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - babel-plugin-transform-strict-mode@6.24.1: + /concurrently@9.1.0: + resolution: {integrity: sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==} + engines: {node: '>=18'} + hasBin: true dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true - babel-preset-env@1.7.0(supports-color@8.1.1): + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: - babel-plugin-check-es2015-constants: 6.22.0 - babel-plugin-syntax-trailing-function-commas: 6.22.0 - babel-plugin-transform-async-to-generator: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-arrow-functions: 6.22.0 - babel-plugin-transform-es2015-block-scoped-functions: 6.22.0 - babel-plugin-transform-es2015-block-scoping: 6.26.0(supports-color@8.1.1) - babel-plugin-transform-es2015-classes: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-computed-properties: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-destructuring: 6.23.0 - babel-plugin-transform-es2015-duplicate-keys: 6.24.1 - babel-plugin-transform-es2015-for-of: 6.23.0 - babel-plugin-transform-es2015-function-name: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-literals: 6.22.0 - babel-plugin-transform-es2015-modules-amd: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-modules-commonjs: 6.26.2(supports-color@8.1.1) - babel-plugin-transform-es2015-modules-systemjs: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-modules-umd: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-object-super: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-parameters: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-es2015-shorthand-properties: 6.24.1 - babel-plugin-transform-es2015-spread: 6.22.0 - babel-plugin-transform-es2015-sticky-regex: 6.24.1 - babel-plugin-transform-es2015-template-literals: 6.22.0 - babel-plugin-transform-es2015-typeof-symbol: 6.23.0 - babel-plugin-transform-es2015-unicode-regex: 6.24.1 - babel-plugin-transform-exponentiation-operator: 6.24.1(supports-color@8.1.1) - babel-plugin-transform-regenerator: 6.26.0 - browserslist: 3.2.8 - invariant: 2.2.4 - semver: 5.7.1 - transitivePeerDependencies: - - supports-color + ini: 1.3.8 + proto-list: 1.2.4 - babel-runtime@6.26.0: + /configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} dependencies: - core-js: 2.6.12 - regenerator-runtime: 0.11.1 + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 - babel-template@6.26.0(supports-color@8.1.1): + /connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} dependencies: - babel-runtime: 6.26.0 - babel-traverse: 6.26.0(supports-color@8.1.1) - babel-types: 6.26.0 - babylon: 6.18.0 - lodash: 4.17.21 + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 transitivePeerDependencies: - supports-color - babel-traverse@6.26.0(supports-color@8.1.1): + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + /console-ui@3.1.2: + resolution: {integrity: sha512-+5j3R4wZJcEYZeXk30whc4ZU/+fWW9JMTNntVuMYpjZJ9n26Cxr0tUBXco1NRjVZRpRVvZ4DDKKKIHNYeUG9Dw==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - babel-code-frame: 6.26.0 - babel-messages: 6.23.0 - babel-runtime: 6.26.0 - babel-types: 6.26.0 - babylon: 6.18.0 - debug: 2.6.9(supports-color@8.1.1) - globals: 9.18.0 - invariant: 2.2.4 + chalk: 2.4.2 + inquirer: 6.5.2 + json-stable-stringify: 1.1.1 + ora: 3.4.0 + through2: 3.0.2 + + /consolidate@0.16.0(lodash@4.17.21)(mustache@4.2.0): + resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==} + engines: {node: '>= 0.10.0'} + deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog + peerDependencies: + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + babel-core: ^6.26.3 + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jade: ^1.11.0 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + marko: ^3.14.4 + mote: ^0.2.0 + mustache: ^4.0.1 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + razor-tmpl: ^1.3.1 + react: ^16.13.1 + react-dom: ^16.13.1 + slm: ^2.0.0 + squirrelly: ^5.1.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-jade: '*' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + arc-templates: + optional: true + atpl: + optional: true + babel-core: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jade: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + marko: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + razor-tmpl: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + squirrelly: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-jade: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + dependencies: + bluebird: 3.7.2 lodash: 4.17.21 - transitivePeerDependencies: - - supports-color + mustache: 4.2.0 - babel-types@6.26.0: + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} dependencies: - babel-runtime: 6.26.0 - esutils: 2.0.3 - lodash: 4.17.21 - to-fast-properties: 1.0.3 + safe-buffer: 5.2.1 - babel6-plugin-strip-class-callcheck@6.0.0: {} + /content-tag@2.0.3: + resolution: {integrity: sha512-htLIdtfhhKW2fHlFLnZH7GFzHSdSpHhDLrWVswkNiiPMZ5uXq5JfrGboQKFhNQuAAFF8VNB2EYUj3MsdJrKKpg==} - babylon@6.18.0: {} + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} - backbone@1.4.1: - dependencies: - underscore: 1.13.6 + /continuable-cache@0.3.1: + resolution: {integrity: sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==} - backburner.js@2.8.0: {} + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true - balanced-match@1.0.2: {} + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - base64-js@1.5.1: {} + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - base64id@2.0.0: {} + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: true - base@0.11.2: - dependencies: - cache-base: 1.0.1 - class-utils: 0.3.6 - component-emitter: 1.3.0 - define-property: 1.0.0 - isobject: 3.0.1 - mixin-deep: 1.3.2 - pascalcase: 0.1.1 + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} - basic-auth@2.0.1: - dependencies: - safe-buffer: 5.1.2 + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + /copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} - better-path-resolve@1.0.0: + /core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} dependencies: - is-windows: 1.0.2 + browserslist: 4.24.2 - big.js@5.2.2: {} + /core-object@3.1.5: + resolution: {integrity: sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==} + engines: {node: '>= 4'} + dependencies: + chalk: 2.4.2 - binary-extensions@2.2.0: {} + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - binaryextensions@2.3.0: {} + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 - bind-decorator@1.0.11: {} + /cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 - bl@4.1.0: + /cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - bluebird@3.7.2: {} + /cryptiles@0.2.2: + resolution: {integrity: sha512-gvWSbgqP+569DdslUiCelxIv3IYK5Lgmq1UrRnk+s1WxQOQ16j3GPDcjdtgL5Au65DU/xQi6q3xPtf5Kta+3IQ==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + requiresBuild: true + dependencies: + boom: 0.4.2 + dev: true + optional: true - blueimp-md5@2.19.0: {} + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} - body-parser@1.20.1: + /css-color-converter@2.0.0: + resolution: {integrity: sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==} dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + color-convert: 0.5.3 + color-name: 1.1.4 + css-unit-converter: 1.1.2 + dev: true - body-parser@1.20.2: + /css-loader@5.2.7(webpack@5.94.0): + resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: 5.94.0 dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color + icss-utils: 5.1.0(postcss@8.4.49) + loader-utils: 2.0.4 + postcss: 8.4.49 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.49) + postcss-modules-local-by-default: 4.1.0(postcss@8.4.49) + postcss-modules-scope: 3.2.1(postcss@8.4.49) + postcss-modules-values: 4.0.0(postcss@8.4.49) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.6.3 + webpack: 5.94.0 - body@5.1.0: + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} dependencies: - continuable-cache: 0.3.1 - error: 7.2.1 - raw-body: 1.1.7 - safe-json-parse: 1.0.1 + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: true - bole@5.0.12: + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} dependencies: - fast-safe-stringify: 2.1.1 - individual: 3.0.0 + mdn-data: 2.0.30 + source-map-js: 1.2.1 + dev: true - boom@0.4.2: - dependencies: - hoek: 0.9.1 - optional: true + /css-unit-converter@1.1.2: + resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true - bower-config@1.4.3: + /csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} dependencies: - graceful-fs: 4.2.11 - minimist: 0.2.4 - mout: 1.2.4 - osenv: 0.1.5 - untildify: 2.1.0 - wordwrap: 0.0.3 + css-tree: 1.1.3 + dev: true - bower-endpoint-parser@0.2.2: {} + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true - boxen@5.1.2: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - string-width: 4.2.3 - type-fest: 0.20.2 - widest-line: 3.1.0 - wrap-ansi: 7.0.0 + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true - brace-expansion@1.1.11: + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + cssom: 0.3.8 + dev: true - brace-expansion@2.0.1: + /cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + engines: {node: '>=18'} dependencies: - balanced-match: 1.0.2 + rrweb-cssom: 0.7.1 - braces@2.3.2: - dependencies: - arr-flatten: 1.1.0 - array-unique: 0.3.2 - extend-shallow: 2.0.1 - fill-range: 4.0.0 - isobject: 3.0.1 - repeat-element: 1.1.4 - snapdragon: 0.8.2 - snapdragon-node: 2.1.1 - split-string: 3.1.0 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color + /ctype@0.5.3: + resolution: {integrity: sha512-T6CEkoSV4q50zW3TlTHMbzy1E5+zlnNcY+yb7tWVYlTwPhx9LpnfAkd4wecpWknDyptp4k97LUZeInlf6jdzBg==} + engines: {node: '>= 0.4'} + requiresBuild: true + dev: true + optional: true - braces@3.0.2: - dependencies: - fill-range: 7.0.1 + /dag-map@2.0.2: + resolution: {integrity: sha512-xnsprIzYuDeiyu5zSKwilV/ajRHxnoMlAhEREfyfTgTSViMVY2fGP1ZcHJbtwup26oCkofySU/m6oKJ3HrkW7w==} + + /data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} - broccoli-amd-funnel@2.0.1: + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} dependencies: - broccoli-plugin: 1.3.1 - symlink-or-copy: 1.3.1 + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true - broccoli-asset-rev@3.0.0: + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} dependencies: - broccoli-asset-rewrite: 2.0.0 - broccoli-filter: 1.3.0 - broccoli-persistent-filter: 1.4.6 - json-stable-stringify: 1.0.2 - minimatch: 3.1.2 - rsvp: 3.6.2 - transitivePeerDependencies: - - supports-color + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 - broccoli-asset-rewrite@2.0.0: + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} dependencies: - broccoli-filter: 1.3.0 - transitivePeerDependencies: - - supports-color + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 - broccoli-babel-transpiler@7.8.1: + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} dependencies: - '@babel/core': 7.24.5 - '@babel/polyfill': 7.12.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-persistent-filter: 2.3.1 - clone: 2.1.2 - hash-for-dep: 1.5.1 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - json-stable-stringify: 1.0.2 - rsvp: 4.8.5 - workerpool: 3.1.2 - transitivePeerDependencies: - - supports-color + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 - broccoli-builder@0.18.14: + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} dependencies: - broccoli-node-info: 1.1.0 - heimdalljs: 0.2.6 - promise-map-series: 0.2.3 - quick-temp: 0.1.8 - rimraf: 2.7.1 - rsvp: 3.6.2 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + /date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: true - broccoli-caching-writer@2.3.1: + /de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: - broccoli-kitchen-sink-helpers: 0.2.9 - broccoli-plugin: 1.1.0 - debug: 2.6.9 - rimraf: 2.7.1 - rsvp: 3.6.2 - walk-sync: 0.2.7 - transitivePeerDependencies: - - supports-color + ms: 2.0.0 - broccoli-caching-writer@3.0.3: + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: - broccoli-kitchen-sink-helpers: 0.3.1 - broccoli-plugin: 1.3.1 - debug: 2.6.9 - rimraf: 2.7.1 - rsvp: 3.6.2 - walk-sync: 0.3.4 - transitivePeerDependencies: - - supports-color + ms: 2.1.3 - broccoli-clean-css@1.1.0: + /debug@4.3.7(supports-color@8.1.1): + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: - broccoli-persistent-filter: 1.4.6 - clean-css-promise: 0.1.1 - inline-source-map-comment: 1.0.5 - json-stable-stringify: 1.0.2 - transitivePeerDependencies: - - supports-color + ms: 2.1.3 + supports-color: 8.1.1 - broccoli-concat@4.2.5: + /debug@4.3.7(supports-color@9.4.0): + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: - broccoli-debug: 0.6.5 - broccoli-kitchen-sink-helpers: 0.3.1 - broccoli-plugin: 4.0.7 - ensure-posix-path: 1.1.1 - fast-sourcemap-concat: 2.1.0 - find-index: 1.1.1 - fs-extra: 8.1.0 - fs-tree-diff: 2.0.1 - lodash.merge: 4.6.2 - lodash.omit: 4.5.0 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - supports-color + ms: 2.1.3 + supports-color: 9.4.0 - broccoli-config-loader@1.0.1: + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + /decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} dependencies: - broccoli-caching-writer: 3.0.3 - transitivePeerDependencies: - - supports-color + mimic-response: 1.0.1 + dev: true - broccoli-config-replace@1.1.2: + /decorator-transforms@2.3.0(@babel/core@7.26.0): + resolution: {integrity: sha512-jo8c1ss9yFPudHuYYcrJ9jpkDZIoi+lOGvt+Uyp9B+dz32i50icRMx9Bfa8hEt7TnX1FyKWKkjV+cUdT/ep2kA==} dependencies: - broccoli-kitchen-sink-helpers: 0.3.1 - broccoli-plugin: 1.3.1 - debug: 2.6.9 - fs-extra: 0.24.0 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + babel-import-util: 3.0.0 transitivePeerDependencies: - - supports-color + - '@babel/core' - broccoli-debug@0.6.5: + /deep-eql@0.1.3: + resolution: {integrity: sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==} dependencies: - broccoli-plugin: 1.3.1 - fs-tree-diff: 0.5.9 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - symlink-or-copy: 1.3.1 - tree-sync: 1.4.0 - transitivePeerDependencies: - - supports-color + type-detect: 0.1.1 + dev: true - broccoli-file-creator@2.1.1: + /deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} dependencies: - broccoli-plugin: 1.3.1 - mkdirp: 0.5.6 + type-detect: 4.1.0 + dev: true + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true - broccoli-filter@1.3.0: + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: - broccoli-kitchen-sink-helpers: 0.3.1 - broccoli-plugin: 1.3.1 - copy-dereference: 1.0.0 - debug: 2.6.9 - mkdirp: 0.5.6 - promise-map-series: 0.2.3 - rsvp: 3.6.2 - symlink-or-copy: 1.3.1 - walk-sync: 0.3.4 - transitivePeerDependencies: - - supports-color + clone: 1.0.4 - broccoli-funnel-reducer@1.0.0: {} + /defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + dev: true - broccoli-funnel@3.0.8: + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} dependencies: - array-equal: 1.0.0 - broccoli-plugin: 4.0.7 - debug: 4.3.4 - fs-tree-diff: 2.0.1 - heimdalljs: 0.2.6 - minimatch: 3.1.2 - walk-sync: 2.2.0 - transitivePeerDependencies: - - supports-color + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 - broccoli-kitchen-sink-helpers@0.2.9: + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} dependencies: - glob: 5.0.15 - mkdirp: 0.5.6 + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 - broccoli-kitchen-sink-helpers@0.3.1: + /define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} dependencies: - glob: 5.0.15 - mkdirp: 0.5.6 + is-descriptor: 0.1.7 - broccoli-merge-trees@4.2.0: + /define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} dependencies: - broccoli-plugin: 4.0.7 - merge-trees: 2.0.0 - transitivePeerDependencies: - - supports-color + is-descriptor: 1.0.3 - broccoli-middleware@2.1.1: + /define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} dependencies: - ansi-html: 0.0.7 - handlebars: 4.7.7 - has-ansi: 3.0.0 - mime-types: 2.1.35 + is-descriptor: 1.0.3 + isobject: 3.0.1 - broccoli-node-api@1.7.0: {} + /del@5.1.0: + resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} + engines: {node: '>=8'} + dependencies: + globby: 10.0.2 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 3.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + dev: false - broccoli-node-info@1.1.0: {} + /delayed-stream@0.0.5: + resolution: {integrity: sha512-v+7uBd1pqe5YtgPacIIbZ8HuHeLFVNe4mUEyFDXL6KiqzEykjbw+5mXZXpGFgNVasdL4jWKgaKIXrEHiynN1LA==} + engines: {node: '>=0.4.0'} + requiresBuild: true + dev: true + optional: true - broccoli-node-info@2.2.0: {} + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} - broccoli-output-wrapper@3.2.5: - dependencies: - fs-extra: 8.1.0 - heimdalljs-logger: 0.1.10 - symlink-or-copy: 1.3.1 - transitivePeerDependencies: - - supports-color + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - broccoli-persistent-filter@1.4.6: - dependencies: - async-disk-cache: 1.3.5 - async-promise-queue: 1.0.5 - broccoli-plugin: 1.3.1 - fs-tree-diff: 0.5.9 - hash-for-dep: 1.5.1 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - mkdirp: 0.5.6 - promise-map-series: 0.2.3 - rimraf: 2.7.1 - rsvp: 3.6.2 - symlink-or-copy: 1.3.1 - walk-sync: 0.3.4 - transitivePeerDependencies: - - supports-color + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} - broccoli-persistent-filter@2.3.1: - dependencies: - async-disk-cache: 1.3.5 - async-promise-queue: 1.0.5 - broccoli-plugin: 1.3.1 - fs-tree-diff: 2.0.1 - hash-for-dep: 1.5.1 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - mkdirp: 0.5.6 - promise-map-series: 0.2.3 - rimraf: 2.7.1 - rsvp: 4.8.5 - symlink-or-copy: 1.3.1 - sync-disk-cache: 1.3.4 - walk-sync: 1.1.4 - transitivePeerDependencies: - - supports-color + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} - broccoli-persistent-filter@3.1.3: - dependencies: - async-disk-cache: 2.1.0 - async-promise-queue: 1.0.5 - broccoli-plugin: 4.0.7 - fs-tree-diff: 2.0.1 - hash-for-dep: 1.5.1 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - promise-map-series: 0.2.3 - rimraf: 3.0.2 - symlink-or-copy: 1.3.1 - sync-disk-cache: 2.1.0 - transitivePeerDependencies: - - supports-color + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + /detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + /detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} - broccoli-plugin@1.1.0: + /dettle@1.0.4: + resolution: {integrity: sha512-ktaWiLYYc/ajSa819+HxwABpqtk3dGIAmo5CbHvT3B6XyQSM7VNGDvCPNu94Ptc+Ti4tjTvAKRUCXU/lrVG4WQ==} + + /diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} dependencies: - promise-map-series: 0.2.3 - quick-temp: 0.1.8 - rimraf: 2.7.1 - symlink-or-copy: 1.3.1 + path-type: 4.0.0 - broccoli-plugin@1.3.1: + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} dependencies: - promise-map-series: 0.2.3 - quick-temp: 0.1.8 - rimraf: 2.7.1 - symlink-or-copy: 1.3.1 + esutils: 2.0.3 + dev: false + + /dom-element-descriptors@0.5.1: + resolution: {integrity: sha512-DLayMRQ+yJaziF4JJX1FMjwjdr7wdTr1y9XvZ+NfHELfOMcYDnCHneAYXAS4FT1gLILh4V0juMZohhH1N5FsoQ==} - broccoli-plugin@2.1.0: + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead dependencies: - promise-map-series: 0.2.3 - quick-temp: 0.1.8 - rimraf: 2.7.1 - symlink-or-copy: 1.3.1 + webidl-conversions: 7.0.0 + dev: true - broccoli-plugin@4.0.7: + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: - broccoli-node-api: 1.7.0 - broccoli-output-wrapper: 3.2.5 - fs-merger: 3.2.1 - promise-map-series: 0.3.0 - quick-temp: 0.1.8 - rimraf: 3.0.2 - symlink-or-copy: 1.3.1 - transitivePeerDependencies: - - supports-color + no-case: 3.0.4 + tslib: 2.8.1 + dev: true - broccoli-rollup@5.0.0: + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} dependencies: - '@types/broccoli-plugin': 3.0.0 - broccoli-plugin: 4.0.7 - fs-tree-diff: 2.0.1 - heimdalljs: 0.2.6 - node-modules-path: 1.0.2 - rollup: 2.79.1 - rollup-pluginutils: 2.8.2 - symlink-or-copy: 1.3.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - supports-color + is-obj: 2.0.0 + + /duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /editions@1.3.4: + resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} + engines: {node: '>=0.8'} - broccoli-slow-trees@3.1.0: + /editions@2.3.1: + resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} + engines: {node: '>=0.8'} dependencies: - heimdalljs: 0.2.6 + errlop: 2.2.0 + semver: 6.3.1 - broccoli-source@1.1.0: {} + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - broccoli-source@2.1.2: {} + /electron-to-chromium@1.5.57: + resolution: {integrity: sha512-xS65H/tqgOwUBa5UmOuNSLuslDo7zho0y/lgQw35pnrqiZh7UOWHCeL/Bt6noJATbA6tpQJGCifsFsIRZj1Fqg==} - broccoli-source@3.0.1: + /ember-auto-import@2.10.0(@glint/template@1.5.0): + resolution: {integrity: sha512-bcBFDYVTFHyqyq8BNvsj6UO3pE6Uqou/cNmee0WaqBgZ+1nQqFz0UE26usrtnFAT+YaFZSkqF2H36QW84k0/cg==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - broccoli-node-api: 1.7.0 + '@babel/core': 7.26.0 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@embroider/shared-internals': 2.8.1(supports-color@8.1.1) + babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.94.0) + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-ember-template-compilation: 2.3.0 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + babel-plugin-syntax-dynamic-import: 6.18.0 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + css-loader: 5.2.7(webpack@5.94.0) + debug: 4.3.7(supports-color@8.1.1) + fs-extra: 10.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + mini-css-extract-plugin: 2.9.2(webpack@5.94.0) + minimatch: 3.1.2 + parse5: 6.0.1 + pkg-entry-points: 1.1.1 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + semver: 7.6.3 + style-loader: 2.0.0(webpack@5.94.0) + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + webpack: 5.94.0 + transitivePeerDependencies: + - '@glint/template' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli - broccoli-sri-hash@2.1.2: + /ember-cache-primitive-polyfill@1.0.1(@babel/core@7.26.0): + resolution: {integrity: sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw==} + engines: {node: 10.* || >= 12} dependencies: - broccoli-caching-writer: 2.3.1 - mkdirp: 0.5.6 - rsvp: 3.6.2 - sri-toolbox: 0.2.0 - symlink-or-copy: 1.3.1 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-version-checker: 5.1.2 + ember-compatibility-helpers: 1.2.7(@babel/core@7.26.0) + silent-error: 1.1.1 transitivePeerDependencies: + - '@babel/core' - supports-color + dev: true - broccoli-stew@1.6.0: + /ember-cached-decorator-polyfill@1.0.2(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0): + resolution: {integrity: sha512-hUX6OYTKltAPAu8vsVZK02BfMTV0OUXrPqvRahYPhgS7D0I6joLjlskd7mhqJMcaXLywqceIy8/s+x8bxF8bpQ==} + engines: {node: 14.* || >= 16} + peerDependencies: + ember-source: '*' dependencies: - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-persistent-filter: 1.4.6 - broccoli-plugin: 1.3.1 - chalk: 2.4.2 - debug: 3.2.7 - ensure-posix-path: 1.1.1 - fs-extra: 5.0.0 - minimatch: 3.1.2 - resolve: 1.22.1 - rsvp: 4.8.5 - symlink-or-copy: 1.3.1 - walk-sync: 0.3.4 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@glimmer/tracking': 1.1.2 + babel-import-util: 1.4.1 + ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.26.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-babel-plugin-helpers: 1.1.1 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) transitivePeerDependencies: + - '@babel/core' + - '@glint/template' - supports-color + dev: true - broccoli-stew@3.0.0: + /ember-cli-babel-plugin-helpers@1.1.1: + resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} + engines: {node: 6.* || 8.* || >= 10.*} + + /ember-cli-babel@8.2.0(@babel/core@7.26.0): + resolution: {integrity: sha512-8H4+jQElCDo6tA7CamksE66NqBXWs7VNpS3a738L9pZCjg2kXIX4zoyHzkORUqCtr0Au7YsCnrlAMi1v2ALo7A==} + engines: {node: 16.* || 18.* || >= 20} + peerDependencies: + '@babel/core': ^7.12.0 dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-runtime': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.12.18 + amd-name-resolver: 1.3.1 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.26.0) + babel-plugin-ember-data-packages-polyfill: 0.1.2 + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-module-resolver: 5.0.2 + broccoli-babel-transpiler: 8.0.0(@babel/core@7.26.0) broccoli-debug: 0.6.5 broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-persistent-filter: 2.3.1 - broccoli-plugin: 2.1.0 - chalk: 2.4.2 - debug: 4.3.4 + broccoli-source: 3.0.1 + calculate-cache-key-for-tree: 2.0.0 + clone: 2.1.2 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 ensure-posix-path: 1.1.1 - fs-extra: 8.1.0 - minimatch: 3.1.2 - resolve: 1.22.1 - rsvp: 4.8.5 - symlink-or-copy: 1.3.1 - walk-sync: 1.1.4 + resolve-package-path: 4.0.3 + semver: 7.6.3 transitivePeerDependencies: - supports-color - broccoli-string-replace@0.1.2: + /ember-cli-blueprint-test-helpers@0.19.2(ember-cli@5.12.0): + resolution: {integrity: sha512-otCKdGcNFK0+MkQo+LLjYbRD9EerApH6Z/odvvlL1hxrN+owHMV5E+jI2rbtdvNEH0/6w5ZqjH4kS232fvtCxQ==} + engines: {node: 6.* || 8.* || >= 10.*} + peerDependencies: + ember-cli: '*' dependencies: - broccoli-persistent-filter: 1.4.6 - minimatch: 3.1.2 + chai: 4.5.0 + chai-as-promised: 7.1.2(chai@4.5.0) + chai-files: 1.4.0 + debug: 4.3.7(supports-color@8.1.1) + ember-cli: 5.12.0 + ember-cli-internal-test-helpers: 0.9.1 + fs-extra: 7.0.1 + testdouble: 3.20.2 + tmp-sync: 1.1.2 transitivePeerDependencies: - supports-color + dev: true - broccoli-terser-sourcemap@4.1.0: + /ember-cli-dependency-checker@3.3.2(ember-cli@5.12.0): + resolution: {integrity: sha512-PwkrW5oYsdPWwt+0Tojufmv/hxVETTjkrEdK7ANQB2VSnqpA5UcYubwpQM9ONuR2J8wyNDMwEHlqIrk/FYtBsQ==} + engines: {node: '>= 6'} + peerDependencies: + ember-cli: ^3.2.0 || >=4.0.0 dependencies: - async-promise-queue: 1.0.5 - broccoli-plugin: 4.0.7 - debug: 4.3.4 - lodash.defaultsdeep: 4.6.1 - matcher-collection: 2.0.1 - source-map-url: 0.4.1 - symlink-or-copy: 1.3.1 - terser: 5.16.8 - walk-sync: 2.2.0 - workerpool: 6.4.0 + chalk: 2.4.2 + ember-cli: 5.12.0 + find-yarn-workspace-root: 1.2.1 + is-git-url: 1.0.0 + resolve: 1.22.8 + semver: 5.7.2 transitivePeerDependencies: - supports-color - broccoli-test-helper@2.0.0: + /ember-cli-fastboot-testing@0.6.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-fastboot@4.1.5)(ember-cli@5.12.0)(ember-source@5.12.0): + resolution: {integrity: sha512-RwOM0Y2fT5PtiMkG7yXWCaCLGFatwKpfPT4Xt6Hxqf2ZAEkbVQWJw/aFaKLLOAo2kfted8nKdkRvZM6Jm647Pg==} + engines: {node: 12.* || 14.* || 16.* || >= 18} + peerDependencies: + '@ember/test-helpers': '>= 3.0.0' + ember-cli: '*' + ember-cli-fastboot: '*' + ember-source: '*' dependencies: - '@types/tmp': 0.0.33 - broccoli: 2.3.0 - fixturify: 0.3.4 - fs-tree-diff: 0.5.9 - tmp: 0.0.33 - walk-sync: 0.3.4 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + body-parser: 1.20.3 + ember-auto-import: 2.10.0(@glint/template@1.5.0) + ember-cli: 5.12.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-fastboot: 4.1.5(@babel/core@7.26.0)(ember-cli@5.12.0)(ember-source@5.12.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + fastboot: 4.1.5 + json-fn: 1.1.1 + minimist: 1.2.8 + nock: 13.5.6 + resolve: 1.22.8 + whatwg-fetch: 3.6.20 transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - '@swc/core' + - bufferutil + - canvas + - esbuild - supports-color + - uglify-js + - utf-8-validate + - webpack-cli + dev: true - broccoli-uglify-sourcemap@4.0.0: + /ember-cli-fastboot@4.1.5(@babel/core@7.26.0)(ember-cli@5.12.0)(ember-source@5.12.0): + resolution: {integrity: sha512-XVigHzn+xXMqvovdrPNQHXRCzVOkU78ij6adU8Qt7PAaF3stR9oPh/35f30aJ2vcL6jwR72glnuCyXpm3EL22A==} + engines: {node: 14.* || 16.* || >= 18} + peerDependencies: + ember-cli: '*' + ember-source: '*' dependencies: - async-promise-queue: 1.0.5 + broccoli-concat: 4.2.5 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 broccoli-plugin: 4.0.7 - debug: 4.3.4 - lodash.defaultsdeep: 4.6.1 - matcher-collection: 2.0.1 - source-map-url: 0.4.1 - symlink-or-copy: 1.3.1 - terser: 5.16.8 - walk-sync: 2.2.0 - workerpool: 6.4.0 + chalk: 4.1.2 + ember-cli: 5.12.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-lodash-subset: 2.0.1 + ember-cli-preprocess-registry: 3.3.0 + ember-cli-version-checker: 5.1.2 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + fastboot: 4.1.5 + fastboot-express-middleware: 4.1.2 + fastboot-transform: 0.1.3 + fs-extra: 10.1.0 + json-stable-stringify: 1.1.1 + md5-hex: 3.0.1 + recast: 0.19.1 + silent-error: 1.1.1 transitivePeerDependencies: + - '@babel/core' + - bufferutil + - canvas - supports-color + - utf-8-validate + dev: true - broccoli@2.3.0: - dependencies: - broccoli-node-info: 1.1.0 - broccoli-slow-trees: 3.1.0 - broccoli-source: 1.1.0 - commander: 2.20.3 - connect: 3.7.0 - esm: 3.2.25 - findup-sync: 2.0.0 - handlebars: 4.7.7 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - mime-types: 2.1.35 - promise.prototype.finally: 3.1.4 - resolve-path: 1.4.0 - rimraf: 2.7.1 - sane: 4.1.0 - tmp: 0.0.33 - tree-sync: 1.4.0 - underscore.string: 3.3.6 - watch-detector: 0.1.0 - transitivePeerDependencies: - - supports-color + /ember-cli-get-component-path-option@1.0.0: + resolution: {integrity: sha512-k47TDwcJ2zPideBCZE8sCiShSxQSpebY2BHcX2DdipMmBox5gsfyVrbKJWIHeSTTKyEUgmBIvQkqTOozEziCZA==} - broccoli@3.5.2: + /ember-cli-htmlbars@6.3.0: + resolution: {integrity: sha512-N9Y80oZfcfWLsqickMfRd9YByVcTGyhYRnYQ2XVPVrp6jyUyOeRWmEAPh7ERSXpp8Ws4hr/JB9QVQrn/yZa+Ag==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - '@types/chai': 4.3.4 - '@types/chai-as-promised': 7.1.5 - '@types/express': 4.17.17 - ansi-html: 0.0.7 - broccoli-node-info: 2.2.0 - broccoli-slow-trees: 3.1.0 - broccoli-source: 3.0.1 - commander: 4.1.1 - connect: 3.7.0 - console-ui: 3.1.2 - esm: 3.2.25 - findup-sync: 4.0.0 - handlebars: 4.7.7 - heimdalljs: 0.2.6 + '@ember/edition-utils': 1.2.0 + babel-plugin-ember-template-compilation: 2.3.0 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + broccoli-debug: 0.6.5 + broccoli-persistent-filter: 3.1.3 + broccoli-plugin: 4.0.7 + ember-cli-version-checker: 5.1.2 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.1 heimdalljs-logger: 0.1.10 - https: 1.0.0 - mime-types: 2.1.35 - resolve-path: 1.4.0 - rimraf: 3.0.2 - sane: 4.1.0 - tmp: 0.0.33 - tree-sync: 2.1.0 - underscore.string: 3.3.6 - watch-detector: 1.0.2 + js-string-escape: 1.0.1 + semver: 7.6.3 + silent-error: 1.1.1 + walk-sync: 2.2.0 transitivePeerDependencies: - supports-color - browser-process-hrtime@1.0.0: {} - - browser-stdout@1.3.1: {} - - browserslist@3.2.8: - dependencies: - caniuse-lite: 1.0.30001614 - electron-to-chromium: 1.4.752 - - browserslist@4.21.5: - dependencies: - caniuse-lite: 1.0.30001470 - electron-to-chromium: 1.4.340 - node-releases: 2.0.10 - update-browserslist-db: 1.0.10(browserslist@4.21.5) - - browserslist@4.23.0: - dependencies: - caniuse-lite: 1.0.30001614 - electron-to-chromium: 1.4.752 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - - buffer-from@1.1.2: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - builtin-modules@3.3.0: {} - - builtins@5.0.1: - dependencies: - semver: 7.6.0 - - bytes@1.0.0: {} - - bytes@3.0.0: {} - - bytes@3.1.2: {} - - cacache@15.3.0: - dependencies: - '@npmcli/fs': 1.1.1 - '@npmcli/move-file': 1.1.2 - chownr: 2.0.0 - fs-minipass: 2.1.0 - glob: 7.2.3 - infer-owner: 1.0.4 - lru-cache: 6.0.0 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - mkdirp: 1.0.4 - p-map: 4.0.0 - promise-inflight: 1.0.1 - rimraf: 3.0.2 - ssri: 8.0.1 - tar: 6.1.13 - unique-filename: 1.1.1 - transitivePeerDependencies: - - bluebird - - cache-base@1.0.1: - dependencies: - collection-visit: 1.0.0 - component-emitter: 1.3.0 - get-value: 2.0.6 - has-value: 1.0.0 - isobject: 3.0.1 - set-value: 2.0.1 - to-object-path: 0.3.0 - union-value: 1.0.1 - unset-value: 1.0.0 - - cacheable-request@6.1.0: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.1.1 - keyv: 3.1.0 - lowercase-keys: 2.0.0 - normalize-url: 4.5.1 - responselike: 1.0.2 - - calculate-cache-key-for-tree@2.0.0: - dependencies: - json-stable-stringify: 1.0.2 - - call-bind@1.0.2: + /ember-cli-inject-live-reload@2.1.0: + resolution: {integrity: sha512-YV5wYRD5PJHmxaxaJt18u6LE6Y+wo455BnmcpN+hGNlChy2piM9/GMvYgTAz/8Vin8RJ5KekqP/w/NEaRndc/A==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.0 - - callsites@3.1.0: {} + clean-base-url: 1.0.0 + ember-cli-version-checker: 3.1.3 - camelcase-keys@6.2.2: + /ember-cli-internal-test-helpers@0.9.1: + resolution: {integrity: sha512-ia+p7LrAe2tENG+Vewdi93kGlsI7OkjB7tEakTtCELkIvZpmPX+uYGhIi5nVOynLiej2M81MQmNqB8jX93ejqQ==} dependencies: - camelcase: 5.3.1 - map-obj: 4.3.0 - quick-lru: 4.0.1 - - camelcase@5.3.1: {} + chai: 3.5.0 + chai-as-promised: 6.0.0(chai@3.5.0) + chai-files: 1.4.0 + chalk: 1.1.3 + debug: 2.6.9 + exists-sync: 0.0.3 + fs-extra: 0.30.0 + lodash: 4.17.21 + rsvp: 3.6.2 + symlink-or-copy: 1.3.1 + through: 2.3.8 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + dev: true - camelcase@6.3.0: {} + /ember-cli-is-package-missing@1.0.0: + resolution: {integrity: sha512-9hEoZj6Au5onlSDdcoBqYEPT8ehlYntZPxH8pBKV0GO7LNel88otSAQsCfXvbi2eKE+MaSeLG/gNaCI5UdWm9g==} - can-symlink@1.0.0: - dependencies: - tmp: 0.0.28 + /ember-cli-lodash-subset@2.0.1: + resolution: {integrity: sha512-QkLGcYv1WRK35g4MWu/uIeJ5Suk2eJXKtZ+8s+qE7C9INmpCPyPxzaqZABquYzcWNzIdw6kYwz3NWAFdKYFxwg==} + engines: {node: ^4.5 || 6.* || >= 7.*} - can-write-to-dir@1.1.1: + /ember-cli-normalize-entity-name@1.0.0: + resolution: {integrity: sha512-rF4P1rW2P1gVX1ynZYPmuIf7TnAFDiJmIUFI1Xz16VYykUAyiOCme0Y22LeZq8rTzwBMiwBwoE3RO4GYWehXZA==} dependencies: - path-temp: 2.1.0 - - caniuse-lite@1.0.30001470: {} - - caniuse-lite@1.0.30001614: {} + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color - capture-exit@2.0.0: - dependencies: - rsvp: 4.8.5 + /ember-cli-path-utils@1.0.0: + resolution: {integrity: sha512-Qq0vvquzf4cFHoDZavzkOy3Izc893r/5spspWgyzLCPTaG78fM3HsrjZm7UWEltbXUqwHHYrqZd/R0jS08NqSA==} - cardinal@1.0.0: + /ember-cli-preprocess-registry@3.3.0: + resolution: {integrity: sha512-60GYpw7VPeB7TvzTLZTuLTlHdOXvayxjAQ+IxM2T04Xkfyu75O2ItbWlftQW7NZVGkaCsXSRAmn22PG03VpLMA==} dependencies: - ansicolors: 0.2.1 - redeyed: 1.0.1 + broccoli-clean-css: 1.1.0 + broccoli-funnel: 3.0.8 + debug: 3.2.7 + process-relative-require: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true - chai-as-promised@6.0.0(chai@3.5.0): + /ember-cli-preprocess-registry@5.0.1: + resolution: {integrity: sha512-Jb2zbE5Kfe56Nf4IpdaQ10zZ72p/RyLdgE5j5/lKG3I94QHlq+7AkAd18nPpb5OUeRUT13yQTAYpU+MbjpKTtg==} + engines: {node: 16.* || >= 18} dependencies: - chai: 3.5.0 - check-error: 1.0.2 + broccoli-funnel: 3.0.8 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - chai-as-promised@7.1.1(chai@4.3.7): + /ember-cli-sri@2.1.1: + resolution: {integrity: sha512-YG/lojDxkur9Bnskt7xB6gUOtJ6aPl/+JyGYm9HNDk3GECVHB3SMN3rlGhDKHa1ndS5NK2W2TSLb9bzRbGlMdg==} + engines: {node: '>= 0.10.0'} dependencies: - chai: 4.3.7 - check-error: 1.0.2 + broccoli-sri-hash: 2.1.2 + transitivePeerDependencies: + - supports-color + dev: true - chai-files@1.4.0: - dependencies: - assertion-error: 1.1.0 + /ember-cli-string-utils@1.1.0: + resolution: {integrity: sha512-PlJt4fUDyBrC/0X+4cOpaGCiMawaaB//qD85AXmDRikxhxVzfVdpuoec02HSiTGTTB85qCIzWBIh8lDOiMyyFg==} - chai@3.5.0: + /ember-cli-terser@4.0.2: + resolution: {integrity: sha512-Ej77K+YhCZImotoi/CU2cfsoZaswoPlGaM5TB3LvjvPDlVPRhxUHO2RsaUVC5lsGeRLRiHCOxVtoJ6GyqexzFA==} + engines: {node: 10.* || 12.* || >= 14} dependencies: - assertion-error: 1.1.0 - deep-eql: 0.1.3 - type-detect: 1.0.0 + broccoli-terser-sourcemap: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true - chai@4.3.7: + /ember-cli-test-info@1.0.0: + resolution: {integrity: sha512-dEVTIpmUfCzweC97NGf6p7L6XKBwV2GmSM4elmzKvkttEp5P7AvGA9uGyN4GqFq+RwhW+2b0I2qlX00w+skm+A==} dependencies: - assertion-error: 1.1.0 - check-error: 1.0.2 - deep-eql: 4.1.3 - get-func-name: 2.0.0 - loupe: 2.3.6 - pathval: 1.1.1 - type-detect: 4.0.8 + ember-cli-string-utils: 1.1.0 - chalk@1.1.3: + /ember-cli-test-loader@3.1.0(@babel/core@7.26.0): + resolution: {integrity: sha512-0aocZV9SIoOHiU3hrH3IuLR6busWhTX6UVXgd490hmJkIymmOXNH2+jJoC7Ebkeo3PiOfAdjqhb765QDlHSJOw==} + engines: {node: 10.* || >= 12} dependencies: - ansi-styles: 2.2.1 - escape-string-regexp: 1.0.5 - has-ansi: 2.0.0 - strip-ansi: 3.0.1 - supports-color: 2.0.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color - chalk@2.4.2: + /ember-cli-typescript-blueprint-polyfill@0.1.0: + resolution: {integrity: sha512-g0weUTOnHmPGqVZzkQTl3Nbk9fzEdFkEXydCs5mT1qBjXh8eQ6VlmjjGD5/998UXKuA0pLSCVVMbSp/linLzGA==} dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + chalk: 4.1.2 + remove-types: 1.0.0 + transitivePeerDependencies: + - supports-color - chalk@4.1.2: + /ember-cli-typescript@5.3.0: + resolution: {integrity: sha512-gFA+ZwmsvvFwo2Jz/B9GMduEn+fPoGb69qWGP0Tp3+Tu5xypDtIKVSZ5086I3Cr19cLXD4HkrOR3YQvdUKzAkQ==} + engines: {node: '>= 12.*'} dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - - chardet@0.7.0: {} + ansi-to-html: 0.6.15 + broccoli-stew: 3.0.0 + debug: 4.3.7(supports-color@8.1.1) + execa: 4.1.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + rsvp: 4.8.5 + semver: 7.6.3 + stagehand: 1.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color - charm@1.0.2: + /ember-cli-version-checker@3.1.3: + resolution: {integrity: sha512-PZNSvpzwWgv68hcXxyjREpj3WWb81A7rtYNQq1lLEgrWIchF8ApKJjWP3NBpHjaatwILkZAV8klair5WFlXAKg==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - inherits: 2.0.4 - - check-error@1.0.2: {} + resolve-package-path: 1.2.7 + semver: 5.7.2 - chokidar@3.5.3: + /ember-cli-version-checker@5.1.2: + resolution: {integrity: sha512-rk7GY+FmLn/2e22HsZs0Ycrz8HQ1W3Fv+2TFOuEFW9optnDXDgkntPBIl6gact/LHsfBM5RKbM3dHsIIeLgl0Q==} + engines: {node: 10.* || >= 12.*} dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - - chownr@2.0.0: {} - - chrome-trace-event@1.0.3: {} - - ci-info@3.8.0: {} + resolve-package-path: 3.1.0 + semver: 7.6.3 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color - class-utils@0.3.6: + /ember-cli@5.12.0: + resolution: {integrity: sha512-48ZOoUZTXsav37RIYY9gyCR35yo64mhzfv5YHtTbsZZwLv/HjvTz27X0CTvkfVQaOWHYDFekxdp9ppaKz84VNA==} + engines: {node: '>= 18'} + hasBin: true dependencies: - arr-union: 3.1.0 - define-property: 0.2.5 - isobject: 3.0.1 - static-extend: 0.1.2 - - clean-base-url@1.0.0: {} + '@pnpm/find-workspace-dir': 6.0.3 + broccoli: 3.5.2 + broccoli-builder: 0.18.14 + broccoli-concat: 4.2.5 + broccoli-config-loader: 1.0.1 + broccoli-config-replace: 1.1.2 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-funnel-reducer: 1.0.0 + broccoli-merge-trees: 4.2.0 + broccoli-middleware: 2.1.1 + broccoli-slow-trees: 3.1.0 + broccoli-source: 3.0.1 + broccoli-stew: 3.0.0 + calculate-cache-key-for-tree: 2.0.0 + capture-exit: 2.0.0 + chalk: 4.1.2 + ci-info: 3.9.0 + clean-base-url: 1.0.0 + compression: 1.7.5 + configstore: 5.0.1 + console-ui: 3.1.2 + content-tag: 2.0.3 + core-object: 3.1.5 + dag-map: 2.0.2 + diff: 5.2.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-lodash-subset: 2.0.1 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-preprocess-registry: 5.0.1 + ember-cli-string-utils: 1.1.0 + ensure-posix-path: 1.1.1 + execa: 5.1.1 + exit: 0.1.2 + express: 4.21.1 + filesize: 10.1.6 + find-up: 5.0.0 + find-yarn-workspace-root: 2.0.0 + fixturify-project: 2.1.1 + fs-extra: 11.2.0 + fs-tree-diff: 2.0.1 + get-caller-file: 2.0.5 + git-repo-info: 2.1.1 + glob: 8.1.0 + heimdalljs: 0.2.6 + heimdalljs-fs-monitor: 1.1.1 + heimdalljs-graph: 1.0.0 + heimdalljs-logger: 0.1.10 + http-proxy: 1.18.1 + inflection: 2.0.1 + inquirer: 9.3.7 + is-git-url: 1.0.0 + is-language-code: 3.1.0 + isbinaryfile: 5.0.4 + lodash: 4.17.21 + markdown-it: 13.0.2 + markdown-it-terminal: 0.4.0(markdown-it@13.0.2) + minimatch: 7.4.6 + morgan: 1.10.0 + nopt: 3.0.6 + npm-package-arg: 10.1.0 + os-locale: 5.0.0 + p-defer: 3.0.0 + portfinder: 1.0.32 + promise-map-series: 0.3.0 + promise.hash.helper: 1.0.8 + quick-temp: 0.1.8 + remove-types: 1.0.0 + resolve: 1.22.8 + resolve-package-path: 4.0.3 + safe-stable-stringify: 2.5.0 + sane: 5.0.1 + semver: 7.6.3 + silent-error: 1.1.1 + sort-package-json: 1.57.0 + symlink-or-copy: 1.3.1 + temp: 0.9.4 + testem: 3.11.0(patch_hash=yfkum5c5nfihh3ce3f64tnp5rq)(lodash@4.17.21) + tiny-lr: 2.0.0 + tree-sync: 2.1.0 + walk-sync: 3.0.0 + watch-detector: 1.0.2 + workerpool: 6.5.1 + yam: 1.0.0 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - marko + - mote + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - whiskers - clean-css-promise@0.1.1: + /ember-compatibility-helpers@1.2.7(@babel/core@7.26.0): + resolution: {integrity: sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA==} + engines: {node: 10.* || >= 12.*} dependencies: - array-to-error: 1.1.1 - clean-css: 3.4.28 - pinkie-promise: 2.0.1 + babel-plugin-debug-macros: 0.2.0(@babel/core@7.26.0) + ember-cli-version-checker: 5.1.2 + find-up: 5.0.0 + fs-extra: 9.1.0 + semver: 5.7.2 + transitivePeerDependencies: + - '@babel/core' + - supports-color - clean-css@3.4.28: + /ember-decorators-polyfill@1.1.5(@babel/core@7.26.0): + resolution: {integrity: sha512-O154i8sLoVjsiKzSqxGRfHGr+N+drT6mRrLDbNgJCnW/V5uLg/ppZFpUsrdxuXnp5Q9us3OfXV1nX2CH+7bUpA==} + engines: {node: 8.* || >= 10.*} dependencies: - commander: 2.8.1 - source-map: 0.4.4 - - clean-stack@2.2.0: {} - - clean-up-path@1.0.0: {} - - cli-boxes@2.2.1: {} + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-version-checker: 3.1.3 + ember-compatibility-helpers: 1.2.7(@babel/core@7.26.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - cli-columns@4.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 + /ember-disable-prototype-extensions@1.1.3: + resolution: {integrity: sha512-SB9NcZ27OtoUk+gfalsc3QU17+54OoqR668qHcuvHByk4KAhGxCKlkm9EBlKJcGr7yceOOAJqohTcCEBqfRw9g==} + engines: {node: '>= 0.10.0'} + dev: true - cli-cursor@2.1.0: + /ember-eslint-parser@0.5.3(@babel/core@7.26.0)(@typescript-eslint/parser@8.14.0)(eslint@9.14.0): + resolution: {integrity: sha512-FYsoiVcGUGDAybPq8X551hcs9NA0SDx77kfU1sHCTLYqfG4zQ0Rcy+lGxoaXaskH7sTf+Up3/oVyjx/+nJ3joA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@babel/core': ^7.23.6 + '@typescript-eslint/parser': '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: - restore-cursor: 2.0.0 + '@babel/core': 7.26.0 + '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@9.14.0) + '@glimmer/syntax': 0.92.3 + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + content-tag: 2.0.3 + eslint-scope: 7.2.2 + html-tags: 3.3.1 + transitivePeerDependencies: + - eslint - cli-cursor@3.1.0: + /ember-exam@9.0.0(@glint/template@1.5.0)(ember-qunit@8.0.2)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {integrity: sha512-zQBZFlig9SMtCgsU4+0jjtyVdF7RnR539ySxnyesO0mmvhArQOPB576XH598FWawUqkMPbEu7rR/X/NDiozK1g==} + engines: {node: '>= 18'} + peerDependencies: + ember-qunit: '*' + ember-source: '*' + qunit: 2.19.4 dependencies: - restore-cursor: 3.1.0 + '@babel/core': 7.26.0 + chalk: 5.3.0 + cli-table3: 0.6.5 + debug: 4.3.7(supports-color@8.1.1) + ember-auto-import: 2.10.0(@glint/template@1.5.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-qunit: 8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + execa: 8.0.1 + fs-extra: 11.2.0 + js-yaml: 4.1.0 + npmlog: 7.0.1 + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + rimraf: 5.0.10 + semver: 7.6.3 + silent-error: 1.1.1 + transitivePeerDependencies: + - '@glint/template' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + dev: true - cli-highlight@2.1.11: + /ember-inflector@4.0.3(@babel/core@7.26.0)(ember-source@5.12.0): + resolution: {integrity: sha512-E+NnmzybMRWn1JyEfDxY7arjOTJLIcGjcXnUxizgjD4TlvO1s3O65blZt+Xq2C2AFSPeqHLC6PXd6XHYM8BxdQ==} + engines: {node: 14.* || 16.* || >= 18} + peerDependencies: + ember-source: '*' dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - - cli-spinners@2.7.0: {} + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color - cli-table3@0.6.3: + /ember-load-initializers@2.1.2(@babel/core@7.26.0): + resolution: {integrity: sha512-CYR+U/wRxLbrfYN3dh+0Tb6mFaxJKfdyz+wNql6cqTrA0BBi9k6J3AaKXj273TqvEpyyXegQFFkZEiuZdYtgJw==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-typescript: 5.3.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color - cli-table@0.3.11: + /ember-load-initializers@3.0.1(ember-source@5.12.0): + resolution: {integrity: sha512-qV3vxJKw5+7TVDdtdLPy8PhVsh58MlK8jwzqh5xeOwJPNP7o0+BlhvwoIlLYTPzGaHdfjEIFCgVSyMRGd74E1g==} + engines: {node: '>= 18.*'} + peerDependencies: + ember-source: '*' dependencies: - colors: 1.0.3 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + dev: true - cli-width@2.2.1: {} - - cli-width@3.0.0: {} - - cliui@7.0.4: + /ember-maybe-import-regenerator@1.0.0(@babel/core@7.26.0): + resolution: {integrity: sha512-wtjgjEV0Hk4fgiAwFjOfPrGWfmFrbRW3zgNZO4oA3H5FlbMssMvWuR8blQ3QSWYHODVK9r+ThsRAs8lG4kbxqA==} + engines: {node: '>= 12.*'} dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + regenerator-runtime: 0.13.11 + transitivePeerDependencies: + - '@babel/core' + - supports-color - cliui@8.0.1: + /ember-modifier@4.2.0(@babel/core@7.26.0)(ember-source@5.12.0): + resolution: {integrity: sha512-BJ48eTEGxD8J7+lofwVmee7xDgNDgpr5dd6+MSu4gk+I6xb35099RMNorXY5hjjwMJEyi/IRR6Yn3M7iJMz8Zw==} + peerDependencies: + ember-source: '*' + peerDependenciesMeta: + ember-source: + optional: true dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + '@embroider/addon-shim': 1.9.0 + decorator-transforms: 2.3.0(@babel/core@7.26.0) + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - clone-response@1.0.3: + /ember-page-title@8.2.3(@glimmer/component@1.1.2)(ember-source@5.12.0): + resolution: {integrity: sha512-9XH4EVPCpSCyXRsLPzdDydU4HgQnaVeJJTrRF0WVh5bZERI9DgxuHv1NPmZU28todHRH91KcBc5nx8kIVJmqUw==} + engines: {node: 16.* || >= 18} + peerDependencies: + '@glimmer/component': '*' + ember-source: '*' dependencies: - mimic-response: 1.0.1 - - clone@1.0.4: {} - - clone@2.1.2: {} - - co@4.6.0: {} + '@embroider/addon-shim': 1.9.0 + '@glimmer/component': 1.1.2(@babel/core@7.26.0) + '@simple-dom/document': 1.4.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - supports-color + dev: true - collection-visit@1.0.0: + /ember-qunit@8.0.2(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {integrity: sha512-Rf60jeUTWNsF3Imf/FLujW/B/DFmKVXKmXO1lirTXjpertKfwRhp/3MnN8a/U/WyodTIsERkInGT1IqTtphCdQ==} + peerDependencies: + '@ember/test-helpers': '>=3.0.3' + ember-source: '*' + qunit: 2.19.4 dependencies: - map-visit: 1.0.0 - object-visit: 1.0.1 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@embroider/addon-shim': 1.9.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-test-loader: 3.1.0(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - color-convert@1.9.3: + /ember-qunit@9.0.1(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {integrity: sha512-9DgjczFG7ZjINmwWFYDtUF8McbYqQir82hyFp/ZbMOLkpFvHCKPw1mtKcpcdLnLAAYJpwR2/MCyPNiEMkR11aA==} + peerDependencies: + '@ember/test-helpers': '>=3.0.3' + ember-source: '*' + qunit: 2.19.4 dependencies: - color-name: 1.1.3 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@embroider/addon-shim': 1.9.0 + '@embroider/macros': 1.16.10(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + qunit-theme-ember: 1.0.0 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - color-convert@2.0.1: + /ember-resolver@11.0.1(@babel/core@7.26.0)(ember-source@5.12.0): + resolution: {integrity: sha512-ucBk3oM+PR+AfYoSUXeQh8cDQS1sSiEKp4Pcgbew5cFMSqPxJfqd1zyZsfQKNTuyubeGmWxBOyMVSTvX2LeCyg==} + engines: {node: 14.* || 16.* || >= 18} + peerDependencies: + ember-source: '*' + peerDependenciesMeta: + ember-source: + optional: true dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color - color-support@1.1.3: {} + /ember-resolver@13.1.0(@babel/core@7.26.0)(ember-source@5.12.0): + resolution: {integrity: sha512-t/PjXLCl5tM9EQXGIFoBgHiA41HkLJpfo17Nud5Cy9eyUPGcnsMjWJqQ+O5QHA0E63Sp+zTn4y/RS5Tu2v2ydg==} + engines: {node: 14.* || 16.* || >= 18} + peerDependencies: + ember-source: '*' + peerDependenciesMeta: + ember-source: + optional: true + dependencies: + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - colors@1.0.3: {} + /ember-rfc176-data@0.3.18: + resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} - combined-stream@0.0.7: + /ember-route-template@1.0.3: + resolution: {integrity: sha512-p//Nk4g4Wu9F8cZdjB69rKxTRi6RRW32a8K5sYsi5cofTcJtPBXRWUXWpQEjJX6qcucgxooQwEm9+7MOy4lwNw==} dependencies: - delayed-stream: 0.0.5 - optional: true + '@embroider/addon-shim': 1.9.0 + transitivePeerDependencies: + - supports-color + dev: true - combined-stream@1.0.8: + /ember-router-generator@2.0.0: + resolution: {integrity: sha512-89oVHVJwmLDvGvAUWgS87KpBoRhy3aZ6U0Ql6HOmU4TrPkyaa8pM0W81wj9cIwjYprcQtN9EwzZMHnq46+oUyw==} + engines: {node: 8.* || 10.* || >= 12} dependencies: - delayed-stream: 1.0.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9(supports-color@8.1.1) + recast: 0.18.10 + transitivePeerDependencies: + - supports-color - command-line-args@5.2.1: + /ember-simple-tree@0.8.4(@babel/core@7.26.0): + resolution: {integrity: sha512-cE3hC5YE+fIkLby3QbMphjdkBikj5j2107Rh39ZDajxmIOGf/DW7t0L5CqArkRy8Wn4HDl4wJSwmrmV/GYapfg==} + engines: {node: 12.* || 14.* || >= 16} dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - - commander@2.20.3: {} + '@glimmer/component': 1.1.2(@babel/core@7.26.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-htmlbars: 6.3.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - commander@2.8.1: + /ember-source-channel-url@3.0.0: + resolution: {integrity: sha512-vF/8BraOc66ZxIDo3VuNP7iiDrnXEINclJgSJmqwAAEpg84Zb1DHPI22XTXSDA+E8fW5btPUxu65c3ZXi8AQFA==} + engines: {node: 10.* || 12.* || >= 14} + hasBin: true dependencies: - graceful-readlink: 1.0.1 - - commander@4.1.1: {} - - commander@7.2.0: {} - - common-tags@1.8.2: {} - - commondir@1.0.1: {} - - component-emitter@1.3.0: {} + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: true - compressible@2.0.18: + /ember-source@5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0): + resolution: {integrity: sha512-2MWlJmQEeeiIk9p5CDMuvD470YPi7/4wXgU41ftbWc9svwF+0usoe4PLoLC0T/jV6YX+3SY5tumQfxLSLoFhmQ==} + engines: {node: '>= 18.*'} + peerDependencies: + '@glimmer/component': '*' dependencies: - mime-db: 1.52.0 + '@babel/core': 7.26.0 + '@ember/edition-utils': 1.2.0 + '@glimmer/compiler': 0.92.4 + '@glimmer/component': 1.1.2(@babel/core@7.26.0) + '@glimmer/destroyable': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/manager': 0.92.4 + '@glimmer/node': 0.92.4 + '@glimmer/opcode-compiler': 0.92.4 + '@glimmer/owner': 0.92.3 + '@glimmer/program': 0.92.4 + '@glimmer/reference': 0.92.3 + '@glimmer/runtime': 0.92.4 + '@glimmer/syntax': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/validator': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/vm-babel-plugins': 0.92.3(@babel/core@7.26.0) + '@simple-dom/interface': 1.4.0 + backburner.js: 2.8.0 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + chalk: 4.1.2 + ember-auto-import: 2.10.0(@glint/template@1.5.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript-blueprint-polyfill: 0.1.0 + ember-cli-version-checker: 5.1.2 + ember-router-generator: 2.0.0 + inflection: 2.0.1 + route-recognizer: 0.3.4 + router_js: 8.0.6(route-recognizer@0.3.4) + semver: 7.6.3 + silent-error: 1.1.1 + simple-html-tokenizer: 0.5.11 + webpack: 5.94.0 + transitivePeerDependencies: + - '@glint/template' + - '@swc/core' + - esbuild + - rsvp + - supports-color + - uglify-js + - webpack-cli - compression@1.7.4: + /ember-source@6.1.0-beta.1(@glimmer/component@1.1.2): + resolution: {integrity: sha512-ErAYSpftkTnxr6rS6eaCkW/p5Cn8keXW/92P3MfkZNXTD3iAwARS2k7E6lYrnmCONPlae1yaSmkGbKf+fkV0rw==} + engines: {node: '>= 18.*'} + peerDependencies: + '@glimmer/component': '*' dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 + '@babel/core': 7.26.0 + '@ember/edition-utils': 1.2.0 + '@embroider/addon-shim': 1.9.0 + '@glimmer/compiler': 0.92.4 + '@glimmer/component': 1.1.2(@babel/core@7.26.0) + '@glimmer/destroyable': 0.92.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.92.3 + '@glimmer/interfaces': 0.92.3 + '@glimmer/manager': 0.92.4 + '@glimmer/node': 0.92.4 + '@glimmer/opcode-compiler': 0.92.4 + '@glimmer/owner': 0.92.3 + '@glimmer/program': 0.92.4 + '@glimmer/reference': 0.92.3 + '@glimmer/runtime': 0.92.4 + '@glimmer/syntax': 0.92.3 + '@glimmer/util': 0.92.3 + '@glimmer/validator': 0.92.3 + '@glimmer/vm': 0.92.3 + '@glimmer/vm-babel-plugins': 0.92.3(@babel/core@7.26.0) + '@simple-dom/interface': 1.4.0 + backburner.js: 2.8.0 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + chalk: 4.1.2 + ember-auto-import: 2.10.0(@glint/template@1.5.0) + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript-blueprint-polyfill: 0.1.0 + ember-cli-version-checker: 5.1.2 + ember-router-generator: 2.0.0 + inflection: 2.0.1 + route-recognizer: 0.3.4 + router_js: 8.0.6(route-recognizer@0.3.4) + semver: 7.6.3 + silent-error: 1.1.1 + simple-html-tokenizer: 0.5.11 + webpack: 5.94.0 transitivePeerDependencies: + - '@glint/template' + - '@swc/core' + - esbuild + - rsvp - supports-color + - uglify-js + - webpack-cli + dev: true - concat-map@0.0.1: {} + /ember-strict-resolver@1.3.0(@babel/core@7.26.0): + resolution: {integrity: sha512-GeI1LLLt470sjaq/huKGQTDJPDOH0FlrX8FFVcSZPXO2U9FQH7Kc8BaXb4GpViJbfLLC4d7tIUZI4NBnuXSmKg==} + engines: {node: 10.* || >= 12} + dependencies: + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - config-chain@1.1.13: + /ember-template-imports@3.4.2: + resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} + engines: {node: 12.* || >= 14} dependencies: - ini: 1.3.8 - proto-list: 1.2.4 + babel-import-util: 0.2.0 + broccoli-stew: 3.0.0 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 + line-column: 1.0.2 + magic-string: 0.25.9 + parse-static-imports: 1.1.0 + string.prototype.matchall: 4.0.11 + validate-peer-dependencies: 1.2.0 + transitivePeerDependencies: + - supports-color + dev: true - configstore@5.0.1: + /ember-template-imports@4.1.3: + resolution: {integrity: sha512-0R7FBozyG2lLH7DxeB8w/PVsdQdG2W+jZx8Y9aPWtfV7qjZlsZ9mfRgn1acF0OD1J5wEUduaSC4MAmWL+A7maQ==} + engines: {node: 16.* || >= 18} dependencies: - dot-prop: 5.3.0 - graceful-fs: 4.2.11 - make-dir: 3.1.0 - unique-string: 2.0.0 - write-file-atomic: 3.0.3 - xdg-basedir: 4.0.0 + broccoli-stew: 3.0.0 + content-tag: 2.0.3 + ember-cli-version-checker: 5.1.2 + transitivePeerDependencies: + - supports-color + dev: true - connect@3.7.0: + /ember-template-lint@6.0.0: + resolution: {integrity: sha512-TWWt/qCd4KoQ50T3We5nCoKcsrAT8Ip79Kmm9eyWjjyL+LAbRFu0z+GxcmW7MR+QCNW/1LQs3kwEdtIcaHEGiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 - parseurl: 1.3.3 - utils-merge: 1.0.1 + '@lint-todo/utils': 13.1.1 + aria-query: 5.3.2 + chalk: 5.3.0 + ci-info: 4.1.0 + date-fns: 3.6.0 + ember-template-imports: 3.4.2 + ember-template-recast: 6.1.5 + eslint-formatter-kakoune: 1.0.0 + find-up: 7.0.0 + fuse.js: 7.0.0 + get-stdin: 9.0.0 + globby: 14.0.2 + is-glob: 4.0.3 + language-tags: 1.0.9 + micromatch: 4.0.8 + resolve: 1.22.8 + v8-compile-cache: 2.4.0 + yargs: 17.7.2 transitivePeerDependencies: - supports-color + dev: true - console-control-strings@1.1.0: {} - - console-ui@3.1.2: + /ember-template-recast@6.1.5: + resolution: {integrity: sha512-VnRN8FzEHQnw/5rCv6Wnq8MVYXbGQbFY+rEufvWV+FO/IsxMahGEud4MYWtTA2q8iG+qJFrDQefNvQ//7MI7Qw==} + engines: {node: 12.* || 14.* || >= 16.*} + hasBin: true dependencies: - chalk: 2.4.2 - inquirer: 6.5.2 - json-stable-stringify: 1.0.2 - ora: 3.4.0 - through2: 3.0.2 + '@glimmer/reference': 0.84.3 + '@glimmer/syntax': 0.84.3 + '@glimmer/validator': 0.92.3 + async-promise-queue: 1.0.5 + colors: 1.4.0 + commander: 8.3.0 + globby: 11.1.0 + ora: 5.4.1 + slash: 3.0.0 + tmp: 0.2.3 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + dev: true - consolidate@0.16.0(mustache@4.2.0): + /ember-tracked-storage-polyfill@1.0.0(@babel/core@7.26.0): + resolution: {integrity: sha512-eL7lZat68E6P/D7b9UoTB5bB5Oh/0aju0Z7PCMi3aTwhaydRaxloE7TGrTRYU+NdJuyNVZXeGyxFxn2frvd3TA==} + engines: {node: 12.* || >= 14} dependencies: - bluebird: 3.7.2 - optionalDependencies: - mustache: 4.2.0 + ember-cli-babel: 8.2.0(@babel/core@7.26.0) + ember-cli-htmlbars: 6.3.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true - content-disposition@0.5.4: + /ember-try-config@4.0.0: + resolution: {integrity: sha512-jAv7fqYJK7QYYekPc/8Nr7KOqDpv/asqM6F8xcRnbmf9UrD35BkSffY63qUuiD9e0aR5qiMNBIQzH8f65rGDqw==} + engines: {node: 10.* || 12.* || >= 14} dependencies: - safe-buffer: 5.2.1 - - content-tag@2.0.1: {} - - content-type@1.0.5: {} - - continuable-cache@0.3.1: {} - - convert-source-map@1.9.0: {} - - convert-source-map@2.0.0: {} - - cookie-signature@1.0.6: {} - - cookie@0.4.2: {} - - cookie@0.5.0: {} - - copy-dereference@1.0.0: {} - - copy-descriptor@0.1.1: {} + ember-source-channel-url: 3.0.0 + lodash: 4.17.21 + package-json: 6.5.0 + remote-git-tags: 3.0.0 + semver: 7.6.3 + transitivePeerDependencies: + - encoding + dev: true - core-js-compat@3.29.1: + /ember-try@3.0.0: + resolution: {integrity: sha512-ZYVKYWMnrHSD3vywo7rV76kPCOC9ATIEnGGG/PEKfCcFE0lB26jltRDnOrhORfLKq0JFp62fFxC/4940U+MwRQ==} + engines: {node: 16.* || >= 18.*} dependencies: - browserslist: 4.23.0 + chalk: 4.1.2 + cli-table3: 0.6.5 + core-object: 3.1.5 + debug: 4.3.7(supports-color@8.1.1) + ember-try-config: 4.0.0 + execa: 4.1.0 + fs-extra: 6.0.1 + resolve: 1.22.8 + rimraf: 3.0.2 + semver: 7.6.3 + walk-sync: 2.2.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: true - core-js-compat@3.37.0: + /ember-welcome-page@7.0.2: + resolution: {integrity: sha512-TyaKxFIRXhODW5BTbqD/by0Gu8Z9B9AA1ki3Bzzm6fOj2b30Qlprtt+XUG52kS0zVNmxYj/WWoT0TsKiU61VOw==} + engines: {node: 14.* || 16.* || >= 18} dependencies: - browserslist: 4.23.0 - - core-js@2.6.12: {} + '@embroider/addon-shim': 1.9.0 + transitivePeerDependencies: + - supports-color + dev: true - core-object@3.1.5: - dependencies: - chalk: 2.4.2 + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - core-util-is@1.0.3: {} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} - cross-spawn@6.0.5: - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.1 - shebang-command: 1.2.0 - which: 1.3.1 + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} - cryptiles@0.2.2: + /encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + requiresBuild: true dependencies: - boom: 0.4.2 + iconv-lite: 0.6.3 + dev: true optional: true - crypto-random-string@2.0.0: {} - - css-loader@5.2.7(webpack@5.77.0): + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: - icss-utils: 5.1.0(postcss@8.4.21) - loader-utils: 2.0.4 - postcss: 8.4.21 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.21) - postcss-modules-local-by-default: 4.0.0(postcss@8.4.21) - postcss-modules-scope: 3.0.0(postcss@8.4.21) - postcss-modules-values: 4.0.0(postcss@8.4.21) - postcss-value-parser: 4.2.0 - schema-utils: 3.1.1 - semver: 7.6.0 - webpack: 5.77.0 + once: 1.4.0 - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 + /engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} - css-tree@2.3.1: + /engine.io@6.6.2: + resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} + engines: {node: '>=10.2.0'} dependencies: - mdn-data: 2.0.30 - source-map-js: 1.0.2 - - cssesc@3.0.0: {} + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.17.6 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7(supports-color@8.1.1) + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate - csso@4.2.0: + /enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} dependencies: - css-tree: 1.1.3 + graceful-fs: 4.2.11 + tapable: 2.2.1 - cssom@0.3.8: {} + /ensure-posix-path@1.1.1: + resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} - cssom@0.4.4: {} + /entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + dev: true - cssom@0.5.0: {} + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - cssstyle@2.3.0: - dependencies: - cssom: 0.3.8 + /entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} - ctype@0.5.3: - optional: true + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} - dag-map@2.0.2: {} + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: true - data-uri-to-buffer@2.0.2: {} + /errlop@2.2.0: + resolution: {integrity: sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==} + engines: {node: '>=0.8'} - data-urls@2.0.0: + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: - abab: 2.0.6 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 + is-arrayish: 0.2.1 - data-urls@3.0.2: + /error@7.2.1: + resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} dependencies: - abab: 2.0.6 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 + string-template: 0.2.1 - debug@2.6.9: + /es-abstract@1.23.4: + resolution: {integrity: sha512-HR1gxH5OaiN7XH7uiWH0RLw0RcFySiSoW1ctxmD1ahTw3uGBtkmm/ng0tDU1OtYx5OK6EOL5Y6O21cDflG3Jcg==} + engines: {node: '>= 0.4'} dependencies: - ms: 2.0.0 + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 - debug@2.6.9(supports-color@8.1.1): + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} dependencies: - ms: 2.0.0 - optionalDependencies: - supports-color: 8.1.1 + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} - debug@3.2.7: + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} dependencies: - ms: 2.1.3 + es-errors: 1.3.0 - debug@4.3.4: + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} dependencies: - ms: 2.1.2 + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 - debug@4.3.4(supports-color@8.1.1): + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: - ms: 2.1.2 - optionalDependencies: - supports-color: 8.1.1 + hasown: 2.0.2 + dev: false - debug@4.3.4(supports-color@9.4.0): + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} dependencies: - ms: 2.1.2 - optionalDependencies: - supports-color: 9.4.0 + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 - decamelize@4.0.0: {} + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} - decimal.js@10.4.3: {} + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - decode-uri-component@0.2.2: {} + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} - decompress-response@3.3.0: - dependencies: - mimic-response: 1.0.1 + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} - deep-eql@0.1.3: + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true dependencies: - type-detect: 0.1.1 + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true - deep-eql@4.1.3: + /eslint-compat-utils@0.5.1(eslint@9.14.0): + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' dependencies: - type-detect: 4.0.8 - - deep-extend@0.6.0: {} - - deep-is@0.1.4: {} + eslint: 9.14.0 + semver: 7.6.3 - deepmerge@4.3.1: {} - - defaults@1.0.4: + /eslint-config-prettier@9.1.0(eslint@9.14.0): + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' dependencies: - clone: 1.0.4 + eslint: 9.14.0 - defer-to-connect@1.1.3: {} + /eslint-formatter-kakoune@1.0.0: + resolution: {integrity: sha512-Uk/TVLt6Nf6Xoz7C1iYuZjOSdJxe5aaauGRke8JhKeJwD66Y61/pY2FjtLP04Ooq9PwV34bzrkKkU2UZ5FtDRA==} + dev: true - define-properties@1.2.0: + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: false - define-property@0.2.5: + /eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-node@0.3.9)(eslint@9.14.0): + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true dependencies: - is-descriptor: 0.1.6 + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + debug: 3.2.7 + eslint: 9.14.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: false - define-property@1.0.0: + /eslint-plugin-ember@12.3.1(@babel/core@7.26.0)(@typescript-eslint/parser@8.14.0)(eslint@9.14.0): + resolution: {integrity: sha512-Ew8E7R0inU7HSQZ7ChixLvv4y3wtyC++9DYBmAYyjtRoM+p/PwP2kUkyKYJTLi5v5IuSR+fS3IWtbswoq9bPyQ==} + engines: {node: 18.* || 20.* || >= 21} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '>= 8' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: - is-descriptor: 1.0.2 + '@ember-data/rfc395-data': 0.0.4 + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + css-tree: 2.3.1 + ember-eslint-parser: 0.5.3(@babel/core@7.26.0)(@typescript-eslint/parser@8.14.0)(eslint@9.14.0) + ember-rfc176-data: 0.3.18 + eslint: 9.14.0 + eslint-utils: 3.0.0(eslint@9.14.0) + estraverse: 5.3.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + requireindex: 1.2.0 + snake-case: 3.0.4 + transitivePeerDependencies: + - '@babel/core' + dev: true - define-property@2.0.2: + /eslint-plugin-es-x@7.8.0(eslint@9.14.0): + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' dependencies: - is-descriptor: 1.0.2 - isobject: 3.0.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/regexpp': 4.12.1 + eslint: 9.14.0 + eslint-compat-utils: 0.5.1(eslint@9.14.0) - del@5.1.0: + /eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0): + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: - globby: 10.0.2 - graceful-fs: 4.2.11 + '@rtsao/scc': 1.1.0 + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.14.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0)(eslint-import-resolver-node@0.3.9)(eslint@9.14.0) + hasown: 2.0.2 + is-core-module: 2.15.1 is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - - delayed-stream@0.0.5: - optional: true - - delayed-stream@1.0.0: {} - - delegates@1.0.0: {} - - depd@1.1.2: {} - - depd@2.0.0: {} - - destroy@1.2.0: {} - - detect-file@1.0.0: {} - - detect-indent@6.1.0: {} - - detect-libc@2.0.3: {} - - detect-newline@3.1.0: {} - - dettle@1.0.2: {} - - diff@5.0.0: {} - - diff@5.1.0: {} + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: false - dir-glob@3.0.1: + /eslint-plugin-mocha@10.5.0(eslint@9.14.0): + resolution: {integrity: sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==} + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: '>=7.0.0' dependencies: - path-type: 4.0.0 + eslint: 9.14.0 + eslint-utils: 3.0.0(eslint@9.14.0) + globals: 13.24.0 + rambda: 7.5.0 + dev: false - doctrine@2.1.0: + /eslint-plugin-n@17.13.1(eslint@9.14.0): + resolution: {integrity: sha512-97qzhk1z3DdSJNCqT45EslwCu5+LB9GDadSyBItgKUfGsXAmN/aa7LRQ0ZxHffUxUzvgbTPJL27/pE9ZQWHy7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + enhanced-resolve: 5.17.1 + eslint: 9.14.0 + eslint-plugin-es-x: 7.8.0(eslint@9.14.0) + get-tsconfig: 4.8.1 + globals: 15.12.0 + ignore: 5.3.2 + minimatch: 9.0.5 + semver: 7.6.3 + + /eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0)(eslint@9.14.0)(prettier@3.3.3): + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true dependencies: - esutils: 2.0.3 + eslint: 9.14.0 + eslint-config-prettier: 9.1.0(eslint@9.14.0) + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + dev: true - doctrine@3.0.0: + /eslint-plugin-qunit@8.1.2(eslint@9.14.0): + resolution: {integrity: sha512-2gDQdHlQW8GVXD7YYkO8vbm9Ldc60JeGMuQN5QlD48OeZ8znBvvoHWZZMeXjvoDPReGaLEvyuWrDtrI8bDbcqw==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} dependencies: - esutils: 2.0.3 + eslint-utils: 3.0.0(eslint@9.14.0) + requireindex: 1.2.0 + transitivePeerDependencies: + - eslint - domexception@2.0.1: + /eslint-plugin-simple-import-sort@12.1.1(eslint@9.14.0): + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' dependencies: - webidl-conversions: 5.0.0 + eslint: 9.14.0 + dev: false - domexception@4.0.0: + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} dependencies: - webidl-conversions: 7.0.0 + esrecurse: 4.3.0 + estraverse: 4.3.0 - dot-case@3.0.4: + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - no-case: 3.0.4 - tslib: 2.5.0 + esrecurse: 4.3.0 + estraverse: 5.3.0 - dot-prop@5.3.0: + /eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - is-obj: 2.0.0 - - duplexer3@0.1.5: {} - - editions@1.3.4: {} + esrecurse: 4.3.0 + estraverse: 5.3.0 - editions@2.3.1: + /eslint-utils@3.0.0(eslint@9.14.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' dependencies: - errlop: 2.2.0 - semver: 6.3.1 + eslint: 9.14.0 + eslint-visitor-keys: 2.1.0 - ee-first@1.1.1: {} + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} - electron-to-chromium@1.4.340: {} + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - electron-to-chromium@1.4.752: {} + /eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - ember-auto-import@2.6.1(webpack@5.77.0): + /eslint@9.14.0: + resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true dependencies: - '@babel/core': 7.21.4 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': 7.21.0(@babel/core@7.21.4) - '@babel/preset-env': 7.21.4(@babel/core@7.21.4) - '@embroider/macros': 1.10.0 - '@embroider/shared-internals': 2.0.0 - babel-loader: 8.3.0(@babel/core@7.21.4)(webpack@5.77.0) - babel-plugin-ember-modules-api-polyfill: 3.5.0 - babel-plugin-htmlbars-inline-precompile: 5.3.1 - babel-plugin-syntax-dynamic-import: 6.18.0 - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-plugin: 4.0.7 - broccoli-source: 3.0.1 - css-loader: 5.2.7(webpack@5.77.0) - debug: 4.3.4 - fs-extra: 10.1.0 - fs-tree-diff: 2.0.1 - handlebars: 4.7.7 - js-string-escape: 1.0.1 - lodash: 4.17.21 - mini-css-extract-plugin: 2.7.5(webpack@5.77.0) - parse5: 6.0.1 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - semver: 7.3.8 - style-loader: 2.0.0(webpack@5.77.0) - typescript-memoize: 1.1.1 - walk-sync: 3.0.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.18.0 + '@eslint/core': 0.7.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.14.0 + '@eslint/plugin-kit': 0.2.2 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.5 + debug: 4.3.7(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + text-table: 0.2.0 transitivePeerDependencies: - supports-color - - webpack - ember-cache-primitive-polyfill@1.0.1(@babel/core@7.21.4): - dependencies: - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - ember-compatibility-helpers: 1.2.6(@babel/core@7.21.4) - silent-error: 1.1.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color + /esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} - ember-cache-primitive-polyfill@1.0.1(@babel/core@7.24.5): + /espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - ember-compatibility-helpers: 1.2.6(@babel/core@7.24.5) - silent-error: 1.1.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 - ember-cached-decorator-polyfill@1.0.1(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)): - dependencies: - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - babel-import-util: 1.3.0 - ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.21.4) - ember-cli-babel: 7.26.11 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - transitivePeerDependencies: - - '@babel/core' - - supports-color + /esprima@3.0.0: + resolution: {integrity: sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw==} + engines: {node: '>=0.10.0'} + hasBin: true - ember-cached-decorator-polyfill@1.0.1(@babel/core@7.24.5)(ember-source@4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0)): + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} dependencies: - '@embroider/macros': 1.10.0 - '@glimmer/tracking': 1.1.2 - babel-import-util: 1.3.0 - ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.24.5) - ember-cli-babel: 7.26.11 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-source: 4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0) - transitivePeerDependencies: - - '@babel/core' - - supports-color + estraverse: 5.3.0 - ember-cli-app-version@6.0.0(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)): + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} dependencies: - ember-cli-babel: 7.26.11 - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - git-repo-info: 2.1.1 - transitivePeerDependencies: - - supports-color + estraverse: 5.3.0 - ember-cli-babel-plugin-helpers@1.1.1: {} + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} - ember-cli-babel@7.26.11: - dependencies: - '@babel/core': 7.21.4 - '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-decorators': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.4) - '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.21.4) - '@babel/plugin-transform-runtime': 7.21.4(@babel/core@7.21.4) - '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.4) - '@babel/polyfill': 7.12.1 - '@babel/preset-env': 7.21.4(@babel/core@7.21.4) - '@babel/runtime': 7.12.18 - amd-name-resolver: 1.3.1 - babel-plugin-debug-macros: 0.3.4(@babel/core@7.21.4) - babel-plugin-ember-data-packages-polyfill: 0.1.2 - babel-plugin-ember-modules-api-polyfill: 3.5.0 - babel-plugin-module-resolver: 3.2.0 - broccoli-babel-transpiler: 7.8.1 - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - broccoli-source: 2.1.2 - calculate-cache-key-for-tree: 2.0.0 - clone: 2.1.2 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-cli-version-checker: 4.1.1 - ensure-posix-path: 1.1.1 - fixturify-project: 1.10.0 - resolve-package-path: 3.1.0 - rimraf: 3.0.2 - semver: 5.7.1 - transitivePeerDependencies: - - supports-color + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} - ember-cli-blueprint-test-helpers@0.19.2: - dependencies: - chai: 4.3.7 - chai-as-promised: 7.1.1(chai@4.3.7) - chai-files: 1.4.0 - debug: 4.3.4 - ember-cli-internal-test-helpers: 0.9.1 - fs-extra: 7.0.1 - testdouble: 3.17.1 - tmp-sync: 1.1.2 - transitivePeerDependencies: - - supports-color + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - ember-cli-dependency-checker@3.3.1(ember-cli@4.11.0): - dependencies: - chalk: 2.4.2 - ember-cli: 4.11.0 - find-yarn-workspace-root: 1.2.1 - is-git-url: 1.0.0 - resolve: 1.22.1 - semver: 5.7.1 - transitivePeerDependencies: - - supports-color + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} - ember-cli-fastboot-testing@0.6.0(webpack@5.77.0): - dependencies: - body-parser: 1.20.2 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - fastboot: 3.3.2 - json-fn: 1.1.1 - minimist: 1.2.8 - nock: 13.3.0 - resolve: 1.22.1 - whatwg-fetch: 3.6.2 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - - webpack + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} - ember-cli-fastboot@4.1.0: - dependencies: - broccoli-concat: 4.2.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-plugin: 4.0.7 - chalk: 4.1.2 - ember-cli-babel: 7.26.11 - ember-cli-lodash-subset: 2.0.1 - ember-cli-preprocess-registry: 3.3.0 - ember-cli-version-checker: 5.1.2 - fastboot: 4.1.0 - fastboot-express-middleware: 4.1.0 - fastboot-transform: 0.1.3 - fs-extra: 10.1.0 - json-stable-stringify: 1.0.2 - md5-hex: 3.0.1 - recast: 0.19.1 - silent-error: 1.1.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - ember-cli-get-component-path-option@1.0.0: {} + /events-to-array@1.1.2: + resolution: {integrity: sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==} - ember-cli-htmlbars@6.2.0: - dependencies: - '@ember/edition-utils': 1.2.0 - babel-plugin-ember-template-compilation: 2.0.0 - babel-plugin-htmlbars-inline-precompile: 5.3.1 - broccoli-debug: 0.6.5 - broccoli-persistent-filter: 3.1.3 - broccoli-plugin: 4.0.7 - ember-cli-version-checker: 5.1.2 - fs-tree-diff: 2.0.1 - hash-for-dep: 1.5.1 - heimdalljs-logger: 0.1.10 - js-string-escape: 1.0.1 - semver: 7.3.8 - silent-error: 1.1.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - supports-color + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /exec-sh@0.3.6: + resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} - ember-cli-inject-live-reload@2.1.0: + /execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} dependencies: - clean-base-url: 1.0.0 - ember-cli-version-checker: 3.1.3 + cross-spawn: 6.0.5 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 - ember-cli-internal-test-helpers@0.9.1: + /execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} dependencies: - chai: 3.5.0 - chai-as-promised: 6.0.0(chai@3.5.0) - chai-files: 1.4.0 - chalk: 1.1.3 - debug: 2.6.9 - exists-sync: 0.0.3 - fs-extra: 0.30.0 - lodash: 4.17.21 - rsvp: 3.6.2 - symlink-or-copy: 1.3.1 - through: 2.3.8 - walk-sync: 0.3.4 - transitivePeerDependencies: - - supports-color + cross-spawn: 7.0.5 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 - ember-cli-is-package-missing@1.0.0: {} + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.5 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 - ember-cli-lodash-subset@2.0.1: {} + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.5 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /execa@9.5.1: + resolution: {integrity: sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==} + engines: {node: ^18.19.0 || >=20.5.0} + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.5 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.1.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + dev: true - ember-cli-normalize-entity-name@1.0.0: - dependencies: - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + /exists-sync@0.0.3: + resolution: {integrity: sha512-/qPB5E0cRuA/Cs5vHrmKYSfhIBCPJs9Vm3e9aIejMwwbe6idMeNbGu1g5stvr/bXT6HywHckLPEkmY7HK6FlwA==} + deprecated: Please replace with usage of fs.existsSync + dev: true - ember-cli-path-utils@1.0.0: {} + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} - ember-cli-preprocess-registry@3.3.0: + /expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} dependencies: - broccoli-clean-css: 1.1.0 - broccoli-funnel: 3.0.8 - debug: 3.2.7 - process-relative-require: 1.0.0 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 transitivePeerDependencies: - supports-color - ember-cli-sri@2.1.1: + /expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} dependencies: - broccoli-sri-hash: 2.1.2 - transitivePeerDependencies: - - supports-color + homedir-polyfill: 1.0.3 - ember-cli-string-utils@1.1.0: {} + /expect-type@0.20.0: + resolution: {integrity: sha512-uHaC9LYNv6BcW+8SvXcwUUDCrrUxt3GSa61DFvTHj8JC+M0hekMFBwMlCarLQDk5bbpZ2vStpnQPIwRuV98YMw==} + engines: {node: '>=12.0.0'} + dev: true - ember-cli-terser@4.0.2: + /express@4.21.1: + resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + engines: {node: '>= 0.10.0'} dependencies: - broccoli-terser-sourcemap: 4.1.0 + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 transitivePeerDependencies: - supports-color - ember-cli-test-info@1.0.0: + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} dependencies: - ember-cli-string-utils: 1.1.0 + is-extendable: 0.1.1 - ember-cli-test-loader@3.0.0: + /extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} dependencies: - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + assign-symbols: 1.0.0 + is-extendable: 1.0.1 - ember-cli-typescript-blueprint-polyfill@0.1.0: + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} dependencies: - chalk: 4.1.2 - remove-types: 1.0.0 - transitivePeerDependencies: - - supports-color + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 - ember-cli-typescript@2.0.2(@babel/core@7.21.4): + /extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} dependencies: - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.4) - '@babel/plugin-transform-typescript': 7.4.5(@babel/core@7.21.4) - ansi-to-html: 0.6.15 - debug: 4.3.4 - ember-cli-babel-plugin-helpers: 1.1.1 - execa: 1.0.0 - fs-extra: 7.0.1 - resolve: 1.22.1 - rsvp: 4.8.5 - semver: 6.3.1 - stagehand: 1.0.1 - walk-sync: 1.1.4 + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 transitivePeerDependencies: - - '@babel/core' - supports-color - ember-cli-typescript@3.0.0(@babel/core@7.21.4): - dependencies: - '@babel/plugin-transform-typescript': 7.5.5(@babel/core@7.21.4) - ansi-to-html: 0.6.15 - debug: 4.3.4 - ember-cli-babel-plugin-helpers: 1.1.1 - execa: 2.1.0 - fs-extra: 8.1.0 - resolve: 1.22.1 - rsvp: 4.8.5 - semver: 6.3.0 - stagehand: 1.0.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - '@babel/core' - - supports-color + /extract-stack@2.0.0: + resolution: {integrity: sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==} + engines: {node: '>=8'} - ember-cli-typescript@3.0.0(@babel/core@7.24.5): - dependencies: - '@babel/plugin-transform-typescript': 7.5.5(@babel/core@7.24.5) - ansi-to-html: 0.6.15 - debug: 4.3.4 - ember-cli-babel-plugin-helpers: 1.1.1 - execa: 2.1.0 - fs-extra: 8.1.0 - resolve: 1.22.1 - rsvp: 4.8.5 - semver: 6.3.0 - stagehand: 1.0.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - '@babel/core' - - supports-color + /fake-xml-http-request@2.1.2: + resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==} + dev: true - ember-cli-version-checker@3.1.3: - dependencies: - resolve-package-path: 1.2.7 - semver: 5.7.1 + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - ember-cli-version-checker@4.1.1: - dependencies: - resolve-package-path: 2.0.0 - semver: 6.3.0 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true - ember-cli-version-checker@5.1.2: + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} dependencies: - resolve-package-path: 3.1.0 - semver: 7.3.8 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - ember-cli@4.11.0: + /fast-sourcemap-concat@2.1.1: + resolution: {integrity: sha512-7h9/x25c6AQwdU3mA8MZDUMR3UCy50f237egBrBkuwjnUZSmfu4ptCf91PZSKzON2Uh5VvIHozYKWcPPgcjxIw==} + engines: {node: 10.* || >= 12.*} dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.24.5) - amd-name-resolver: 1.3.1 - babel-plugin-module-resolver: 4.1.0 - bower-config: 1.4.3 - bower-endpoint-parser: 0.2.2 - broccoli: 3.5.2 - broccoli-amd-funnel: 2.0.1 - broccoli-babel-transpiler: 7.8.1 - broccoli-builder: 0.18.14 - broccoli-concat: 4.2.5 - broccoli-config-loader: 1.0.1 - broccoli-config-replace: 1.1.2 - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - broccoli-funnel-reducer: 1.0.0 - broccoli-merge-trees: 4.2.0 - broccoli-middleware: 2.1.1 - broccoli-slow-trees: 3.1.0 - broccoli-source: 3.0.1 - broccoli-stew: 3.0.0 - calculate-cache-key-for-tree: 2.0.0 - capture-exit: 2.0.0 - chalk: 4.1.2 - ci-info: 3.8.0 - clean-base-url: 1.0.0 - compression: 1.7.4 - configstore: 5.0.1 - console-ui: 3.1.2 - core-object: 3.1.5 - dag-map: 2.0.2 - diff: 5.1.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-lodash-subset: 2.0.1 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-preprocess-registry: 3.3.0 - ember-cli-string-utils: 1.1.0 - ember-source-channel-url: 3.0.0 - ensure-posix-path: 1.1.1 - execa: 5.1.1 - exit: 0.1.2 - express: 4.18.2 - filesize: 10.0.6 - find-up: 5.0.0 - find-yarn-workspace-root: 2.0.0 - fixturify-project: 2.1.1 - fs-extra: 10.1.0 - fs-tree-diff: 2.0.1 - get-caller-file: 2.0.5 - git-repo-info: 2.1.1 - glob: 8.1.0 - heimdalljs: 0.2.6 - heimdalljs-fs-monitor: 1.1.1 - heimdalljs-graph: 1.0.0 + chalk: 2.4.2 + fs-extra: 5.0.0 heimdalljs-logger: 0.1.10 - http-proxy: 1.18.1 - inflection: 2.0.1 - inquirer: 8.2.5 - is-git-url: 1.0.0 - is-language-code: 3.1.0 - isbinaryfile: 5.0.0 - js-yaml: 4.1.0 - leek: 0.0.24 - lodash.template: 4.5.0 - markdown-it: 13.0.1 - markdown-it-terminal: 0.4.0(markdown-it@13.0.1) - minimatch: 5.1.6 - morgan: 1.10.0 - nopt: 3.0.6 - npm-package-arg: 10.1.0 - os-locale: 5.0.0 - p-defer: 3.0.0 - portfinder: 1.0.32 - promise-map-series: 0.3.0 - promise.hash.helper: 1.0.8 - quick-temp: 0.1.8 - remove-types: 1.0.0 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - safe-stable-stringify: 2.4.3 - sane: 5.0.1 - semver: 7.3.8 - silent-error: 1.1.1 - sort-package-json: 1.57.0 - symlink-or-copy: 1.3.1 - temp: 0.9.4 - testem: 3.10.1 - tiny-lr: 2.0.0 - tree-sync: 2.1.0 - uuid: 8.3.2 - walk-sync: 3.0.0 - watch-detector: 1.0.2 - workerpool: 6.4.0 - yam: 1.0.0 + memory-streams: 0.1.3 + mkdirp: 0.5.6 + source-map: 0.4.4 + source-map-url: 0.3.0 transitivePeerDependencies: - - arc-templates - - atpl - - babel-core - - bracket-template - - bufferutil - - coffee-script - - debug - - dot - - dust - - dustjs-helpers - - dustjs-linkedin - - eco - - ect - - ejs - - encoding - - haml-coffee - - hamlet - - hamljs - - handlebars - - hogan.js - - htmling - - jade - - jazz - - jqtpl - - just - - liquid-node - - liquor - - lodash - - marko - - mote - - nunjucks - - plates - - pug - - qejs - - ractive - - razor-tmpl - - react - - react-dom - - slm - - squirrelly - supports-color - - swig - - swig-templates - - teacup - - templayed - - then-jade - - then-pug - - tinyliquid - - toffee - - twig - - twing - - underscore + + /fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + /fastboot-express-middleware@4.1.2: + resolution: {integrity: sha512-vnzEBV7gZ3lSoGiqG/7+006nHNA3z+ZnU/5u9jPHtKpjH28yEbvZq6PnAeTu24UR98jZVR0pnFbfX0co+O9PeA==} + engines: {node: 12.* || 14.* || >=16} + dependencies: + chalk: 4.1.2 + fastboot: 4.1.2 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color - utf-8-validate - - vash - - velocityjs - - walrus - - whiskers + dev: true - ember-cli@4.11.0(debug@4.3.4): + /fastboot-transform@0.1.3: + resolution: {integrity: sha512-6otygPIJw1ARp1jJb+6KVO56iKBjhO+5x59RSC9qiZTbZRrv+HZAuP00KD3s+nWMvcFDemtdkugki9DNFTTwCQ==} + dependencies: + broccoli-stew: 1.6.0 + convert-source-map: 1.9.0 + transitivePeerDependencies: + - supports-color + dev: true + + /fastboot@4.1.2: + resolution: {integrity: sha512-VJLmF0xdCNwIIuA7DQtN1KTAKfEGsbZGJ0cfKh64h6DeMh3Fhr2FCCxkPh8zYqGoqzjXFdFbtk60WS3f6HKqBg==} + engines: {node: 12.* || 14.* || >=16} dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.24.5) - amd-name-resolver: 1.3.1 - babel-plugin-module-resolver: 4.1.0 - bower-config: 1.4.3 - bower-endpoint-parser: 0.2.2 - broccoli: 3.5.2 - broccoli-amd-funnel: 2.0.1 - broccoli-babel-transpiler: 7.8.1 - broccoli-builder: 0.18.14 - broccoli-concat: 4.2.5 - broccoli-config-loader: 1.0.1 - broccoli-config-replace: 1.1.2 - broccoli-debug: 0.6.5 - broccoli-funnel: 3.0.8 - broccoli-funnel-reducer: 1.0.0 - broccoli-merge-trees: 4.2.0 - broccoli-middleware: 2.1.1 - broccoli-slow-trees: 3.1.0 - broccoli-source: 3.0.1 - broccoli-stew: 3.0.0 - calculate-cache-key-for-tree: 2.0.0 - capture-exit: 2.0.0 chalk: 4.1.2 - ci-info: 3.8.0 - clean-base-url: 1.0.0 - compression: 1.7.4 - configstore: 5.0.1 - console-ui: 3.1.2 - core-object: 3.1.5 - dag-map: 2.0.2 - diff: 5.1.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-lodash-subset: 2.0.1 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-preprocess-registry: 3.3.0 - ember-cli-string-utils: 1.1.0 - ember-source-channel-url: 3.0.0 - ensure-posix-path: 1.1.1 - execa: 5.1.1 - exit: 0.1.2 - express: 4.18.2 - filesize: 10.0.6 - find-up: 5.0.0 - find-yarn-workspace-root: 2.0.0 - fixturify-project: 2.1.1 - fs-extra: 10.1.0 - fs-tree-diff: 2.0.1 - get-caller-file: 2.0.5 - git-repo-info: 2.1.1 - glob: 8.1.0 - heimdalljs: 0.2.6 - heimdalljs-fs-monitor: 1.1.1 - heimdalljs-graph: 1.0.0 - heimdalljs-logger: 0.1.10 - http-proxy: 1.18.1(debug@4.3.4) - inflection: 2.0.1 - inquirer: 8.2.5 - is-git-url: 1.0.0 - is-language-code: 3.1.0 - isbinaryfile: 5.0.0 - js-yaml: 4.1.0 - leek: 0.0.24 - lodash.template: 4.5.0 - markdown-it: 13.0.1 - markdown-it-terminal: 0.4.0(markdown-it@13.0.1) - minimatch: 5.1.6 - morgan: 1.10.0 - nopt: 3.0.6 - npm-package-arg: 10.1.0 - os-locale: 5.0.0 - p-defer: 3.0.0 - portfinder: 1.0.32 - promise-map-series: 0.3.0 - promise.hash.helper: 1.0.8 - quick-temp: 0.1.8 - remove-types: 1.0.0 - resolve: 1.22.1 - resolve-package-path: 4.0.3 - safe-stable-stringify: 2.4.3 - sane: 5.0.1 - semver: 7.3.8 - silent-error: 1.1.1 - sort-package-json: 1.57.0 - symlink-or-copy: 1.3.1 - temp: 0.9.4 - testem: 3.10.1(debug@4.3.4) - tiny-lr: 2.0.0 - tree-sync: 2.1.0 - uuid: 8.3.2 - walk-sync: 3.0.0 - watch-detector: 1.0.2 - workerpool: 6.4.0 - yam: 1.0.0 + cookie: 0.4.2 + debug: 4.3.7(supports-color@8.1.1) + jsdom: 19.0.0 + resolve: 1.22.8 + simple-dom: 1.4.0 + source-map-support: 0.5.21 transitivePeerDependencies: - - arc-templates - - atpl - - babel-core - - bracket-template - bufferutil - - coffee-script - - debug - - dot - - dust - - dustjs-helpers - - dustjs-linkedin - - eco - - ect - - ejs - - encoding - - haml-coffee - - hamlet - - hamljs - - handlebars - - hogan.js - - htmling - - jade - - jazz - - jqtpl - - just - - liquid-node - - liquor - - lodash - - marko - - mote - - nunjucks - - plates - - pug - - qejs - - ractive - - razor-tmpl - - react - - react-dom - - slm - - squirrelly + - canvas - supports-color - - swig - - swig-templates - - teacup - - templayed - - then-jade - - then-pug - - tinyliquid - - toffee - - twig - - twing - - underscore - utf-8-validate - - vash - - velocityjs - - walrus - - whiskers + dev: true + + /fastboot@4.1.5: + resolution: {integrity: sha512-2FkJWrpxgJjy5kLb3KrYp0pKdB4WgT/6qxtQO7ozYtQqMBOAARMnp59xp/Hdosa1cE2jslZgwDAv3v11OlQfAw==} + engines: {node: 12.* || 14.* || >=16} + dependencies: + chalk: 4.1.2 + cookie: 0.4.2 + debug: 4.3.7(supports-color@8.1.1) + jsdom: 19.0.0 + resolve: 1.22.8 + simple-dom: 1.4.0 + source-map-support: 0.5.21 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + + /faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + + /figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + dependencies: + escape-string-regexp: 1.0.5 + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + + /figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + dependencies: + is-unicode-supported: 2.1.0 + dev: true + + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + + /filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + + /fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /find-babel-config@2.1.2: + resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} + dependencies: + json5: 2.2.3 + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + /find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + dev: true + + /find-index@1.1.1: + resolution: {integrity: sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==} + + /find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: true + + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 - ember-compatibility-helpers@1.2.6(@babel/core@7.21.4): + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} dependencies: - babel-plugin-debug-macros: 0.2.0(@babel/core@7.21.4) - ember-cli-version-checker: 5.1.2 - find-up: 5.0.0 - fs-extra: 9.1.0 - semver: 5.7.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color + locate-path: 5.0.0 + path-exists: 4.0.0 - ember-compatibility-helpers@1.2.6(@babel/core@7.24.5): + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} dependencies: - babel-plugin-debug-macros: 0.2.0(@babel/core@7.24.5) - ember-cli-version-checker: 5.1.2 - find-up: 5.0.0 - fs-extra: 9.1.0 - semver: 5.7.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color + locate-path: 6.0.0 + path-exists: 4.0.0 - ember-data@file:packages/-ember-data(@babel/core@7.21.4)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(webpack@5.77.0): + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - '@ember-data/adapter': file:packages/adapter(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2) - '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.77.0) - '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.8) - '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(ember-inflector@4.0.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/private-build-infra': file:packages/private-build-infra - '@ember-data/request': file:packages/request - '@ember-data/serializer': file:packages/serializer(@ember-data/store@file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2) - '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@file:packages/tracking)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - '@ember-data/tracking': file:packages/tracking - '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 - '@embroider/macros': 1.10.0 - '@glimmer/env': 0.1.7 - broccoli-merge-trees: 4.2.0 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-inflector: 4.0.2 - transitivePeerDependencies: - - '@babel/core' - - '@glimmer/tracking' - - ember-source - - supports-color - - webpack + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true - ember-decorators-polyfill@1.1.5(@babel/core@7.21.4): + /find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} dependencies: - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 3.1.3 - ember-compatibility-helpers: 1.2.6(@babel/core@7.21.4) + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + dev: true + + /find-yarn-workspace-root@1.2.1: + resolution: {integrity: sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==} + dependencies: + fs-extra: 4.0.3 + micromatch: 3.1.10 transitivePeerDependencies: - - '@babel/core' - supports-color - ember-destroyable-polyfill@2.0.3(@babel/core@7.21.4): + /find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} dependencies: - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - ember-compatibility-helpers: 1.2.6(@babel/core@7.21.4) + micromatch: 4.0.8 + + /findup-sync@2.0.0: + resolution: {integrity: sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==} + engines: {node: '>= 0.10'} + dependencies: + detect-file: 1.0.0 + is-glob: 3.1.0 + micromatch: 3.1.10 + resolve-dir: 1.0.1 transitivePeerDependencies: - - '@babel/core' - supports-color + dev: true - ember-disable-prototype-extensions@1.1.3: {} + /findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 - ember-export-application-global@2.0.1: {} + /fireworm@0.7.2: + resolution: {integrity: sha512-GjebTzq+NKKhfmDxjKq3RXwQcN9xRmZWhnnuC9L+/x5wBQtR0aaQM50HsjrzJ2wc28v1vSdfOpELok0TKR4ddg==} + dependencies: + async: 0.2.10 + is-type: 0.0.1 + lodash.debounce: 3.1.1 + lodash.flatten: 3.0.2 + minimatch: 3.1.2 - ember-get-config@2.1.1: + /fixturify-project@2.1.1: + resolution: {integrity: sha512-sP0gGMTr4iQ8Kdq5Ez0CVJOZOGWqzP5dv/veOTdFNywioKjkNWCHBi1q65DMpcNGUGeoOUWehyji274Q2wRgxA==} + engines: {node: 10.* || >= 12.*} dependencies: - '@embroider/macros': 1.10.0 - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + fixturify: 2.1.1 + tmp: 0.0.33 + type-fest: 0.11.0 - ember-inflector@4.0.2: + /fixturify@0.3.4: + resolution: {integrity: sha512-Gx+KSB25b6gMc4bf7UFRTA85uE0iZR+RYur0JHh6dg4AGBh0EksOv4FCHyM7XpGmiJO7Bc7oV7vxENQBT+2WEQ==} dependencies: - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + fs-extra: 0.30.0 + matcher-collection: 1.1.2 + dev: true - ember-load-initializers@2.1.2(@babel/core@7.21.4): + /fixturify@2.1.1: + resolution: {integrity: sha512-SRgwIMXlxkb6AUgaVjIX+jCEqdhyXu9hah7mcK+lWynjKtX73Ux1TDv71B7XyaQ+LJxkYRHl5yCL8IycAvQRUw==} + engines: {node: 10.* || >= 12.*} dependencies: - ember-cli-babel: 7.26.11 - ember-cli-typescript: 2.0.2(@babel/core@7.21.4) - transitivePeerDependencies: - - '@babel/core' - - supports-color + '@types/fs-extra': 8.1.5 + '@types/minimatch': 3.0.5 + '@types/rimraf': 2.0.5 + fs-extra: 8.1.0 + matcher-collection: 2.0.1 + walk-sync: 2.2.0 - ember-maybe-import-regenerator@1.0.0: + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} dependencies: - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - ember-cli-babel: 7.26.11 - regenerator-runtime: 0.13.11 - transitivePeerDependencies: - - supports-color + flatted: 3.3.1 + keyv: 4.5.4 + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + /follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true - ember-qunit@6.2.0(@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)))(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0))(qunit@2.19.4)(webpack@5.77.0): + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: - '@ember/test-helpers': 2.9.3(@babel/core@7.21.4)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)) - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - common-tags: 1.8.2 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-cli-test-loader: 3.0.0 - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - qunit: 2.19.4 - resolve-package-path: 4.0.3 - silent-error: 1.1.1 - validate-peer-dependencies: 2.2.0 - transitivePeerDependencies: - - supports-color - - webpack + is-callable: 1.2.7 + + /for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} - ember-resolver@10.0.0(@ember/string@3.1.1)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)): + /foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} dependencies: - '@ember/string': 3.1.1 - ember-cli-babel: 7.26.11 - optionalDependencies: - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - transitivePeerDependencies: - - supports-color + cross-spawn: 7.0.5 + signal-exit: 4.1.0 + dev: true + + /forever-agent@0.5.2: + resolution: {integrity: sha512-PDG5Ef0Dob/JsZUxUltJOhm/Y9mlteAE+46y3M9RBz/Rd3QVENJ75aGRhN56yekTUboaBIkd8KVWX2NjF6+91A==} + dev: true - ember-resolver@10.0.0(@ember/string@4.0.0)(ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0)): + /form-data@0.1.4: + resolution: {integrity: sha512-x8eE+nzFtAMA0YYlSxf/Qhq6vP1f8wSoZ7Aw1GuctBcmudCNuTUmmx45TfEplyb6cjsZO/jvh6+1VpZn24ez+w==} + engines: {node: '>= 0.8'} + requiresBuild: true dependencies: - '@ember/string': 4.0.0 - ember-cli-babel: 7.26.11 - optionalDependencies: - ember-source: 4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0) - transitivePeerDependencies: - - supports-color + async: 0.9.2 + combined-stream: 0.0.7 + mime: 1.2.11 + dev: true + optional: true + + /form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 - ember-rfc176-data@0.3.18: {} + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} - ember-router-generator@2.0.0: + /fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} dependencies: - '@babel/parser': 7.24.5 - '@babel/traverse': 7.24.5 - recast: 0.18.10 - transitivePeerDependencies: - - supports-color + map-cache: 0.2.2 - ember-simple-tree@0.8.3(@babel/core@7.21.4): + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + /fs-extra@0.24.0: + resolution: {integrity: sha512-w1RvhdLZdU9V3vQdL+RooGlo6b9R9WVoBanOfoJvosWlqSKvrjFlci2oVhwvLwZXBtM7khyPvZ8r3fwsim3o0A==} dependencies: - '@glimmer/component': 1.1.2(@babel/core@7.21.4) - ember-cli-babel: 7.26.11 - ember-cli-htmlbars: 6.2.0 - transitivePeerDependencies: - - '@babel/core' - - supports-color + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 - ember-source-channel-url@3.0.0: + /fs-extra@0.30.0: + resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==} dependencies: - node-fetch: 2.6.9 - transitivePeerDependencies: - - encoding + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + klaw: 1.3.1 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + dev: true - ember-source@4.12.0(@babel/core@7.21.4)(@glimmer/component@1.1.2(@babel/core@7.21.4))(webpack@5.77.0): + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} dependencies: - '@babel/helper-module-imports': 7.21.4 - '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.21.4) - '@ember/edition-utils': 1.2.0 - '@glimmer/component': 1.1.2(@babel/core@7.21.4) - '@glimmer/vm-babel-plugins': 0.84.2(@babel/core@7.21.4) - babel-plugin-debug-macros: 0.3.4(@babel/core@7.21.4) - babel-plugin-filter-imports: 4.0.0 - broccoli-concat: 4.2.5 - broccoli-debug: 0.6.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - chalk: 4.1.2 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-cli-get-component-path-option: 1.0.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-typescript-blueprint-polyfill: 0.1.0 - ember-cli-version-checker: 5.1.2 - ember-router-generator: 2.0.0 - inflection: 1.13.4 - resolve: 1.22.1 - semver: 7.3.8 - silent-error: 1.1.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - webpack + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 - ember-source@4.12.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(webpack@5.77.0): + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} dependencies: - '@babel/helper-module-imports': 7.21.4 - '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.24.5) - '@ember/edition-utils': 1.2.0 - '@glimmer/component': 1.1.2(@babel/core@7.24.5) - '@glimmer/vm-babel-plugins': 0.84.2(@babel/core@7.24.5) - babel-plugin-debug-macros: 0.3.4(@babel/core@7.24.5) - babel-plugin-filter-imports: 4.0.0 - broccoli-concat: 4.2.5 - broccoli-debug: 0.6.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - chalk: 4.1.2 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-cli-get-component-path-option: 1.0.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-typescript-blueprint-polyfill: 0.1.0 - ember-cli-version-checker: 5.1.2 - ember-router-generator: 2.0.0 - inflection: 1.13.4 - resolve: 1.22.1 - semver: 7.3.8 - silent-error: 1.1.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - webpack + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 - ember-source@5.7.0(@babel/core@7.24.5)(@glimmer/component@1.1.2(@babel/core@7.24.5))(rsvp@4.8.5)(webpack@5.77.0): + /fs-extra@4.0.3: + resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} dependencies: - '@babel/helper-module-imports': 7.24.3 - '@ember/edition-utils': 1.2.0 - '@glimmer/compiler': 0.87.1 - '@glimmer/component': 1.1.2(@babel/core@7.24.5) - '@glimmer/destroyable': 0.87.1 - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.87.1 - '@glimmer/interfaces': 0.87.1 - '@glimmer/manager': 0.87.1 - '@glimmer/node': 0.87.1 - '@glimmer/opcode-compiler': 0.87.1 - '@glimmer/owner': 0.87.1 - '@glimmer/program': 0.87.1 - '@glimmer/reference': 0.87.1 - '@glimmer/runtime': 0.87.1 - '@glimmer/syntax': 0.87.1 - '@glimmer/util': 0.87.1 - '@glimmer/validator': 0.87.1 - '@glimmer/vm': 0.87.1 - '@glimmer/vm-babel-plugins': 0.87.1(@babel/core@7.24.5) - '@simple-dom/interface': 1.4.0 - babel-plugin-debug-macros: 0.3.4(@babel/core@7.24.5) - babel-plugin-ember-template-compilation: 2.2.2 - babel-plugin-filter-imports: 4.0.0 - backburner.js: 2.8.0 - broccoli-concat: 4.2.5 - broccoli-debug: 0.6.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - chalk: 4.1.2 - ember-auto-import: 2.6.1(webpack@5.77.0) - ember-cli-babel: 7.26.11 - ember-cli-get-component-path-option: 1.0.0 - ember-cli-is-package-missing: 1.0.0 - ember-cli-normalize-entity-name: 1.0.0 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-typescript-blueprint-polyfill: 0.1.0 - ember-cli-version-checker: 5.1.2 - ember-router-generator: 2.0.0 - inflection: 2.0.1 - route-recognizer: 0.3.4 - router_js: 8.0.5(route-recognizer@0.3.4)(rsvp@4.8.5) - semver: 7.6.0 - silent-error: 1.1.1 - simple-html-tokenizer: 0.5.11 - transitivePeerDependencies: - - '@babel/core' - - rsvp - - supports-color - - webpack + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 - ember-strict-resolver@1.3.0: + /fs-extra@5.0.0: + resolution: {integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==} dependencies: - ember-cli-babel: 7.26.11 - transitivePeerDependencies: - - supports-color + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + /fs-extra@6.0.1: + resolution: {integrity: sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 - ember-template-imports@3.4.2: + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} dependencies: - babel-import-util: 0.2.0 - broccoli-stew: 3.0.0 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-cli-version-checker: 5.1.2 - line-column: 1.0.2 - magic-string: 0.25.9 - parse-static-imports: 1.1.0 - string.prototype.matchall: 4.0.8 - validate-peer-dependencies: 1.2.0 - transitivePeerDependencies: - - supports-color + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 - ember-try-config@4.0.0: + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} dependencies: - ember-source-channel-url: 3.0.0 - lodash: 4.17.21 - package-json: 6.5.0 - remote-git-tags: 3.0.0 - semver: 7.6.0 - transitivePeerDependencies: - - encoding + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 - ember-try@2.0.0: + /fs-merger@3.2.1: + resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==} dependencies: - chalk: 4.1.2 - cli-table3: 0.6.3 - core-object: 3.1.5 - debug: 4.3.4 - ember-try-config: 4.0.0 - execa: 4.1.0 - fs-extra: 9.1.0 - resolve: 1.22.1 - rimraf: 3.0.2 + broccoli-node-api: 1.7.0 + broccoli-node-info: 2.2.0 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 walk-sync: 2.2.0 transitivePeerDependencies: - - encoding - supports-color - emoji-regex@8.0.0: {} - - emojis-list@3.0.0: {} + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true - encodeurl@1.0.2: {} + /fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + dev: false - encoding@0.1.13: + /fs-sync@1.0.6: + resolution: {integrity: sha512-OgbfyvmGVryknZfDXVVhua6OW8946R+AF3O2xxrCW/XFxCYZ4CO2Jrl7kYhrpjZLYvB9gxvWpLikEc9YL9HzCA==} dependencies: - iconv-lite: 0.6.3 - optional: true + glob: 7.2.3 + iconv-lite: 0.4.24 + lodash: 4.17.21 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true - end-of-stream@1.4.4: + /fs-tree-diff@0.5.9: + resolution: {integrity: sha512-872G8ax0kHh01m9n/2KDzgYwouKza0Ad9iFltBpNykvROvf2AGtoOzPJgGx125aolGPER3JuC7uZFrQ7bG1AZw==} dependencies: - once: 1.4.0 - - engine.io-parser@5.0.6: {} + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color - engine.io@6.4.1: + /fs-tree-diff@2.0.1: + resolution: {integrity: sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - '@types/cookie': 0.4.1 - '@types/cors': 2.8.13 - '@types/node': 18.15.10 - accepts: 1.3.8 - base64id: 2.0.0 - cookie: 0.4.2 - cors: 2.8.5 - debug: 4.3.4 - engine.io-parser: 5.0.6 - ws: 8.11.0 + '@types/symlink-or-copy': 1.2.2 + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate - enhanced-resolve@5.12.0: + /fs-updater@1.0.4: + resolution: {integrity: sha512-0pJX4mJF/qLsNEwTct8CdnnRdagfb+LmjRPJ8sO+nCnAZLW0cTmz4rTgU25n+RvTuWSITiLKrGVJceJPBIPlKg==} + engines: {node: '>=6.0.0'} dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 + can-symlink: 1.0.0 + clean-up-path: 1.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + rimraf: 2.7.1 + transitivePeerDependencies: + - supports-color - ensure-posix-path@1.1.1: {} + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - entities@1.1.2: {} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true - entities@2.2.0: {} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - entities@3.0.1: {} + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + functions-have-names: 1.2.3 - err-code@2.0.3: {} + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - errlop@2.2.0: {} + /fuse.js@7.0.0: + resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==} + engines: {node: '>=10'} + dev: true - error-ex@1.3.2: + /gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. dependencies: - is-arrayish: 0.2.1 + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 - error@7.2.1: + /gauge@5.0.2: + resolution: {integrity: sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. dependencies: - string-template: 0.2.1 + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 4.1.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: true - es-abstract@1.21.2: - dependencies: - array-buffer-byte-length: 1.0.0 - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.2.0 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.10 - is-weakref: 1.0.2 - object-inspect: 1.12.3 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.4.3 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.7 - string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.9 + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} - es-module-lexer@0.9.3: {} + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} - es-set-tostringtag@2.0.1: - dependencies: - get-intrinsic: 1.2.0 - has: 1.0.3 - has-tostringtag: 1.0.0 + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true - es-shim-unscopables@1.0.0: + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} dependencies: - has: 1.0.3 + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 - es-to-primitive@1.2.1: + /get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - - escalade@3.1.1: {} - - escape-html@1.0.3: {} + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 - escape-string-regexp@1.0.5: {} + /get-stdin@4.0.1: + resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} + engines: {node: '>=0.10.0'} + dev: true - escape-string-regexp@4.0.0: {} + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + dev: true - escodegen@2.0.0: + /get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionator: 0.8.3 - optionalDependencies: - source-map: 0.6.1 + pump: 3.0.2 - eslint-config-prettier@8.8.0(eslint@8.37.0): + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} dependencies: - eslint: 8.37.0 + pump: 3.0.2 - eslint-import-resolver-node@0.3.7: - dependencies: - debug: 3.2.7 - is-core-module: 2.11.0 - resolve: 1.22.1 - transitivePeerDependencies: - - supports-color + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} - eslint-module-utils@2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint@8.37.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - transitivePeerDependencies: - - supports-color + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true - eslint-plugin-ember@11.4.9(eslint@8.37.0): + /get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} dependencies: - '@ember-data/rfc395-data': 0.0.4 - '@glimmer/syntax': 0.84.3 - css-tree: 2.3.1 - ember-rfc176-data: 0.3.18 - ember-template-imports: 3.4.2 - eslint: 8.37.0 - eslint-utils: 3.0.0(eslint@8.37.0) - estraverse: 5.3.0 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - magic-string: 0.30.0 - requireindex: 1.2.0 - snake-case: 3.0.4 - transitivePeerDependencies: - - supports-color + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + dev: true - eslint-plugin-es@3.0.1(eslint@8.37.0): + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} dependencies: - eslint: 8.37.0 - eslint-utils: 2.1.0 - regexpp: 3.2.0 + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint@8.37.0): + /get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} dependencies: - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.37.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.1(eslint@8.37.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint@8.37.0) - has: 1.0.3 - is-core-module: 2.11.0 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.1 - semver: 6.3.0 - tsconfig-paths: 3.14.2 - optionalDependencies: - '@typescript-eslint/parser': 5.57.1(eslint@8.37.0)(typescript@5.0.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color + resolve-pkg-maps: 1.0.0 - eslint-plugin-mocha@10.1.0(eslint@8.37.0): - dependencies: - eslint: 8.37.0 - eslint-utils: 3.0.0(eslint@8.37.0) - rambda: 7.5.0 + /get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} - eslint-plugin-node@11.1.0(eslint@8.37.0): - dependencies: - eslint: 8.37.0 - eslint-plugin-es: 3.0.1(eslint@8.37.0) - eslint-utils: 2.1.0 - ignore: 5.2.4 - minimatch: 3.1.2 - resolve: 1.22.1 - semver: 6.3.0 + /git-hooks-list@1.0.3: + resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} - eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0(eslint@8.37.0))(eslint@8.37.0)(prettier@2.8.7): - dependencies: - eslint: 8.37.0 - prettier: 2.8.7 - prettier-linter-helpers: 1.0.0 - optionalDependencies: - eslint-config-prettier: 8.8.0(eslint@8.37.0) + /git-repo-info@1.4.1: + resolution: {integrity: sha512-oqzBH6cNvE8Cq3p61ps4m0POZrVMKlARntc2BxLnuqTK+HeWpKfUMJQ7H1CvescHRINj+0a7TKA+Pp/bOq5F1Q==} + dev: true - eslint-plugin-qunit@7.3.4(eslint@8.37.0): - dependencies: - eslint-utils: 3.0.0(eslint@8.37.0) - requireindex: 1.2.0 - transitivePeerDependencies: - - eslint + /git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} - eslint-plugin-simple-import-sort@10.0.0(eslint@8.37.0): + /git-repo-version@1.0.2: + resolution: {integrity: sha512-OPtwtHx9E8/rTMcWT+BU6GNj6Kq/O40bHJZaZAGy+pN2RXGmeKcfr0ix4M+SQuFY8vl5L/wfPSGOAtvUT/e3Qg==} + engines: {node: ^ 4.5 || 6.* || >= 7.*} dependencies: - eslint: 8.37.0 + git-repo-info: 1.4.1 + dev: true - eslint-scope@5.1.1: + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 + is-glob: 4.0.3 - eslint-scope@7.1.1: + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 + is-glob: 4.0.3 - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - eslint-utils@3.0.0(eslint@8.37.0): + /glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true dependencies: - eslint: 8.37.0 - eslint-visitor-keys: 2.1.0 + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + dev: true - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.4.0: {} - - eslint@8.37.0: + /glob@5.0.15: + resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.37.0) - '@eslint-community/regexpp': 4.4.1 - '@eslint/eslintrc': 2.0.2 - '@eslint/js': 8.37.0 - '@humanwhocodes/config-array': 0.11.8 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-visitor-keys: 3.4.0 - espree: 9.5.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.20.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-sdsl: 4.4.0 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 + inflight: 1.0.6 + inherits: 2.0.4 minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color + once: 1.4.0 + path-is-absolute: 1.0.1 - esm@3.2.25: {} + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 - espree@9.5.1: + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported dependencies: - acorn: 8.8.2 - acorn-jsx: 5.3.2(acorn@8.8.2) - eslint-visitor-keys: 3.4.0 + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 - esprima@3.0.0: {} + /glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 - esprima@4.0.1: {} + /global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 - esquery@1.5.0: + /global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} dependencies: - estraverse: 5.3.0 + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} - esrecurse@4.3.0: + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} dependencies: - estraverse: 5.3.0 + type-fest: 0.20.2 + dev: false - estraverse@4.3.0: {} + /globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} - estraverse@5.3.0: {} + /globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} - estree-walker@0.6.1: {} + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 - estree-walker@2.0.2: {} + /globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - esutils@2.0.3: {} + /globby@10.0.0: + resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} + engines: {node: '>=8'} + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 - etag@1.8.1: {} + /globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + dev: false - eventemitter3@4.0.7: {} + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + dev: true - events-to-array@1.1.2: {} + /globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + dev: true - events@3.3.0: {} + /globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - exec-sh@0.3.6: {} + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 - execa@1.0.0: + /got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} dependencies: - cross-spawn: 6.0.5 + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 get-stream: 4.1.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + dev: true - execa@2.1.0: - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 3.1.0 - onetime: 5.1.2 - p-finally: 2.0.1 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - execa@4.1.0: - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - execa@5.1.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + /graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + dev: true - exists-sync@0.0.3: {} + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - exit@0.1.2: {} + /growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} - expand-brackets@2.1.4: + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true dependencies: - debug: 2.6.9 - define-property: 0.2.5 - extend-shallow: 2.0.1 - posix-character-classes: 0.1.1 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 - expand-tilde@2.0.2: + /has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} dependencies: - homedir-polyfill: 1.0.3 + ansi-regex: 2.1.1 + dev: true - express@4.18.2: + /has-ansi@3.0.0: + resolution: {integrity: sha512-5JRDTvNq6mVkaMHQVXrGnaCXHD6JfqxwCy8LA/DQSqLLqePR9uaJVm2u3Ek/UziJFQz+d1ul99RtfIhE2aorkQ==} + engines: {node: '>=4'} dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.1 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.5.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.11.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color + ansi-regex: 3.0.1 - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - extend-shallow@3.0.2: - dependencies: - assign-symbols: 1.0.0 - is-extendable: 1.0.1 + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true - extglob@2.0.4: + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: - array-unique: 0.3.2 - define-property: 1.0.0 - expand-brackets: 2.1.4 - extend-shallow: 2.0.1 - fragment-cache: 0.2.1 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color + es-define-property: 1.0.0 - extract-stack@2.0.0: {} + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} - fake-xml-http-request@2.1.2: {} + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} - fast-deep-equal@3.1.3: {} + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 - fast-diff@1.2.0: {} + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - fast-glob@3.2.12: + /has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 - fast-json-stable-stringify@2.1.0: {} + /has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 - fast-levenshtein@2.0.6: {} + /has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} - fast-safe-stringify@2.1.1: {} + /has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 - fast-sourcemap-concat@1.4.0: + /hash-for-dep@1.5.1: + resolution: {integrity: sha512-/dQ/A2cl7FBPI2pO0CANkvuuVi/IFS5oTyJ0PsOb6jW6WbVW1js5qJXMJTNbWHXBIPdFTWFbabjB+mE0d+gelw==} dependencies: - chalk: 2.4.2 - fs-extra: 5.0.0 + broccoli-kitchen-sink-helpers: 0.3.1 + heimdalljs: 0.2.6 heimdalljs-logger: 0.1.10 - memory-streams: 0.1.3 - mkdirp: 0.5.6 - source-map: 0.4.4 - source-map-url: 0.3.0 - sourcemap-validator: 1.1.1 + path-root: 0.1.1 + resolve: 1.22.8 + resolve-package-path: 1.2.7 transitivePeerDependencies: - supports-color - fast-sourcemap-concat@2.1.0: + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} dependencies: - chalk: 2.4.2 - fs-extra: 5.0.0 - heimdalljs-logger: 0.1.10 - memory-streams: 0.1.3 - mkdirp: 0.5.6 - source-map: 0.4.4 - source-map-url: 0.3.0 - sourcemap-validator: 1.1.1 - transitivePeerDependencies: - - supports-color + function-bind: 1.1.2 + + /hawk@1.1.1: + resolution: {integrity: sha512-am8sVA2bCJIw8fuuVcKvmmNnGFUGW8spTkVtj2fXTEZVkfN42bwFZFtDem57eFi+NSxurJB8EQ7Jd3uCHLn8Vw==} + engines: {node: '>=0.8.0'} + deprecated: This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. + requiresBuild: true + dependencies: + boom: 0.4.2 + cryptiles: 0.2.2 + hoek: 0.9.1 + sntp: 0.2.4 + dev: true + optional: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true - fastboot-express-middleware@4.1.0: + /heimdalljs-fs-monitor@1.1.1: + resolution: {integrity: sha512-BHB8oOXLRlrIaON0MqJSEjGVPDyqt2Y6gu+w2PaEZjrCxeVtZG7etEZp7M4ZQ80HNvnr66KIQ2lot2qdeG8HgQ==} dependencies: - chalk: 4.1.2 - fastboot: 4.1.0 + callsites: 3.1.0 + clean-stack: 2.2.0 + extract-stack: 2.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - utf-8-validate - fastboot-transform@0.1.3: - dependencies: - broccoli-stew: 1.6.0 - convert-source-map: 1.9.0 - transitivePeerDependencies: - - supports-color + /heimdalljs-graph@1.0.0: + resolution: {integrity: sha512-v2AsTERBss0ukm/Qv4BmXrkwsT5x6M1V5Om6E8NcDQ/ruGkERsfsuLi5T8jx8qWzKMGYlwzAd7c/idymxRaPzA==} + engines: {node: 8.* || >= 10.*} - fastboot@3.3.2: + /heimdalljs-logger@0.1.10: + resolution: {integrity: sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==} dependencies: - chalk: 4.1.2 - cookie: 0.4.2 - debug: 4.3.4 - jsdom: 19.0.0 - resolve: 1.22.1 - simple-dom: 1.4.0 - source-map-support: 0.5.21 + debug: 2.6.9 + heimdalljs: 0.2.6 transitivePeerDependencies: - - bufferutil - - canvas - supports-color - - utf-8-validate - fastboot@4.1.0: + /heimdalljs@0.2.6: + resolution: {integrity: sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==} dependencies: - chalk: 4.1.2 - cookie: 0.4.2 - debug: 4.3.4 - jsdom: 19.0.0 - resolve: 1.22.1 - simple-dom: 1.4.0 - source-map-support: 0.5.21 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate + rsvp: 3.2.1 - fastq@1.15.0: - dependencies: - reusify: 1.0.4 + /highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + dev: true - faye-websocket@0.11.4: - dependencies: - websocket-driver: 0.7.4 + /hoek@0.9.1: + resolution: {integrity: sha512-ZZ6eGyzGjyMTmpSPYVECXy9uNfqBR7x5CavhUaLOeD6W0vWK1mp/b7O3f86XE0Mtfo9rZ6Bh3fnuw9Xr8MF9zA==} + engines: {node: '>=0.8.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + requiresBuild: true + dev: true + optional: true - fb-watchman@2.0.2: + /homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} dependencies: - bser: 2.1.1 + parse-passwd: 1.0.0 + + /hono@4.6.9: + resolution: {integrity: sha512-p/pN5yZLuZaHzyAOT2nw2/Ud6HhJHYmDNGH6Ck1OWBhPMVeM1r74jbCRwNi0gyFRjjbsGgoHbOyj7mT1PDNbTw==} + engines: {node: '>=16.9.0'} - figures@2.0.0: + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} dependencies: - escape-string-regexp: 1.0.5 + lru-cache: 6.0.0 + dev: true - figures@3.2.0: + /hosted-git-info@6.1.1: + resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - escape-string-regexp: 1.0.5 + lru-cache: 7.18.3 - file-entry-cache@6.0.1: + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} dependencies: - flat-cache: 3.0.4 + whatwg-encoding: 2.0.0 + dev: true - filesize@10.0.6: {} + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 - filesize@10.1.1: {} + /html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} - filesize@5.0.3: {} + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true - fill-range@4.0.0: + /http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} dependencies: - extend-shallow: 2.0.1 - is-number: 3.0.0 - repeat-string: 1.6.1 - to-regex-range: 2.1.1 + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 - fill-range@7.0.1: + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} dependencies: - to-regex-range: 5.0.1 + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - finalhandler@1.1.2: + /http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.3.0 - parseurl: 1.3.3 - statuses: 1.5.0 - unpipe: 1.0.0 + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color + dev: true - finalhandler@1.2.0: + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color + dev: true - find-babel-config@1.2.0: + /http-proxy-agent@7.0.2(supports-color@8.1.1): + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} dependencies: - json5: 0.5.1 - path-exists: 3.0.0 + agent-base: 7.1.1(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - find-cache-dir@3.3.2: + /http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - - find-index@1.1.1: {} + eventemitter3: 4.0.7 + follow-redirects: 1.15.9 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug - find-replace@3.0.0: + /http-signature@0.10.1: + resolution: {integrity: sha512-coK8uR5rq2IMj+Hen+sKPA5ldgbCc1/spPdKCL1Fw6h+D0s/2LzMcRK0Cqufs1h0ryx/niwBHGFu8HC3hwU+lA==} + engines: {node: '>=0.8'} + requiresBuild: true dependencies: - array-back: 3.1.0 + asn1: 0.1.11 + assert-plus: 0.1.5 + ctype: 0.5.3 + dev: true + optional: true - find-up@2.1.0: + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} dependencies: - locate-path: 2.0.0 + agent-base: 6.0.2 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true - find-up@3.0.0: + /https-proxy-agent@7.0.5(supports-color@8.1.1): + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} dependencies: - locate-path: 3.0.0 + agent-base: 7.1.1(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 + /https@1.0.0: + resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 + /human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} - find-yarn-workspace-root@1.2.1: - dependencies: - fs-extra: 4.0.3 - micromatch: 3.1.10 - transitivePeerDependencies: - - supports-color + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} - find-yarn-workspace-root@2.0.0: - dependencies: - micromatch: 4.0.5 + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true - findup-sync@2.0.0: - dependencies: - detect-file: 1.0.0 - is-glob: 3.1.0 - micromatch: 3.1.10 - resolve-dir: 1.0.1 - transitivePeerDependencies: - - supports-color + /human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + engines: {node: '>=18.18.0'} + dev: true - findup-sync@4.0.0: + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.5 - resolve-dir: 1.0.1 + ms: 2.1.3 + dev: true - fireworm@0.7.2: + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} dependencies: - async: 0.2.10 - is-type: 0.0.1 - lodash.debounce: 3.1.1 - lodash.flatten: 3.0.2 - minimatch: 3.1.2 + safer-buffer: 2.1.2 - fixturify-project@1.10.0: + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} dependencies: - fixturify: 1.3.0 - tmp: 0.0.33 + safer-buffer: 2.1.2 - fixturify-project@2.1.1: + /icss-utils@5.1.0(postcss@8.4.49): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 dependencies: - fixturify: 2.1.1 - tmp: 0.0.33 - type-fest: 0.11.0 + postcss: 8.4.49 - fixturify@0.3.4: - dependencies: - fs-extra: 0.30.0 - matcher-collection: 1.1.2 + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - fixturify@1.3.0: - dependencies: - '@types/fs-extra': 5.1.0 - '@types/minimatch': 3.0.5 - '@types/rimraf': 2.0.5 - fs-extra: 7.0.1 - matcher-collection: 2.0.1 + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} - fixturify@2.1.1: + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} dependencies: - '@types/fs-extra': 8.1.2 - '@types/minimatch': 3.0.5 - '@types/rimraf': 2.0.5 - fs-extra: 8.1.0 - matcher-collection: 2.0.1 - walk-sync: 2.2.0 + parent-module: 1.0.1 + resolve-from: 4.0.0 - flat-cache@3.0.4: - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: false - flat@5.0.2: {} + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} - flatted@3.2.7: {} + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} - follow-redirects@1.15.2: {} + /individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} - follow-redirects@1.15.2(debug@4.3.4): - optionalDependencies: - debug: 4.3.4 + /infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + dev: true + + /inflection@2.0.1: + resolution: {integrity: sha512-wzkZHqpb4eGrOKBl34xy3umnYHx8Si5R1U4fwmdxLo5gdH6mEK8gclckTj/qWqy4Je0bsDYe/qazZYuO7xe3XQ==} + engines: {node: '>=14.0.0'} - for-each@0.3.3: + /inflection@3.0.0: + resolution: {integrity: sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==} + engines: {node: '>=18.0.0'} + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. dependencies: - is-callable: 1.2.7 + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + /ini@3.0.1: + resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - for-in@1.0.2: {} + /inline-source-map-comment@1.0.5: + resolution: {integrity: sha512-a3/m6XgooVCXkZCduOb7pkuvUtNKt4DaqaggKKJrMQHQsqt6JcJXEreExeZiiK4vWL/cM/uF6+chH05pz2/TdQ==} + hasBin: true + dependencies: + chalk: 1.1.3 + get-stdin: 4.0.1 + minimist: 1.2.8 + sum-up: 1.0.3 + xtend: 4.0.2 + dev: true - forever-agent@0.5.2: {} + /inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.17.21 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 - form-data@0.1.4: + /inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} dependencies: - async: 0.9.2 - combined-stream: 0.0.7 - mime: 1.2.11 - optional: true + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 - form-data@3.0.1: + /inquirer@9.3.7: + resolution: {integrity: sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==} + engines: {node: '>=18'} dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 + '@inquirer/figures': 1.0.8 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + external-editor: 3.1.0 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 - form-data@4.0.0: + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 - forwarded@0.2.0: {} + /invert-kv@3.0.1: + resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} + engines: {node: '>=8'} - fragment-cache@0.2.1: + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} dependencies: - map-cache: 0.2.2 - - fresh@0.5.2: {} - - fromentries@1.3.2: {} + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: true - fs-extra@0.24.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 2.4.0 - path-is-absolute: 1.0.1 - rimraf: 2.7.1 + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} - fs-extra@0.30.0: + /is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} dependencies: - graceful-fs: 4.2.11 - jsonfile: 2.4.0 - klaw: 1.3.1 - path-is-absolute: 1.0.1 - rimraf: 2.7.1 + hasown: 2.0.2 - fs-extra@10.1.0: + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 - fs-extra@11.2.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - fs-extra@4.0.3: + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + has-bigints: 1.0.2 - fs-extra@5.0.0: + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + requiresBuild: true dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + binary-extensions: 2.3.0 - fs-extra@7.0.1: + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} - fs-merger@3.2.1: + /is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} dependencies: - broccoli-node-api: 1.7.0 - broccoli-node-info: 2.2.0 - fs-extra: 8.1.0 - fs-tree-diff: 2.0.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - supports-color + hasown: 2.0.2 - fs-minipass@2.1.0: + /is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} dependencies: - minipass: 3.3.6 + hasown: 2.0.2 - fs-readdir-recursive@1.1.0: {} - - fs-sync@1.0.6: + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} dependencies: - glob: 7.2.3 - iconv-lite: 0.4.24 - lodash: 4.17.21 - mkdirp: 0.5.6 - rimraf: 2.7.1 + is-typed-array: 1.1.13 - fs-tree-diff@0.5.9: + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} dependencies: - heimdalljs-logger: 0.1.10 - object-assign: 4.1.1 - path-posix: 1.0.0 - symlink-or-copy: 1.3.1 - transitivePeerDependencies: - - supports-color + has-tostringtag: 1.0.2 - fs-tree-diff@2.0.1: + /is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} dependencies: - '@types/symlink-or-copy': 1.2.0 - heimdalljs-logger: 0.1.10 - object-assign: 4.1.1 - path-posix: 1.0.0 - symlink-or-copy: 1.3.1 - transitivePeerDependencies: - - supports-color + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 - fs-updater@1.0.4: + /is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} dependencies: - can-symlink: 1.0.0 - clean-up-path: 1.0.0 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - rimraf: 2.7.1 - transitivePeerDependencies: - - supports-color - - fs.realpath@1.0.0: {} - - fsevents@2.3.2: - optional: true - - function-bind@1.1.1: {} + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 - function.prototype.name@1.1.5: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - functions-have-names: 1.2.3 + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true - functions-have-names@1.2.3: {} + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} - gauge@4.0.4: + /is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 + is-plain-object: 2.0.4 - gensync@1.0.0-beta.2: {} + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} - get-caller-file@2.0.5: {} + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} - get-func-name@2.0.0: {} + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} - get-intrinsic@1.2.0: - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-symbols: 1.0.3 + /is-git-url@1.0.0: + resolution: {integrity: sha512-UCFta9F9rWFSavp9H3zHEHrARUfZbdJvmHKeEpds4BK3v7W2LdXoNypMtXXi5w5YBDEBCTYmbI+vsSwI8LYJaQ==} + engines: {node: '>=0.8'} - get-source@2.0.12: + /is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} dependencies: - data-uri-to-buffer: 2.0.2 - source-map: 0.6.1 - - get-stdin@4.0.1: {} + is-extglob: 2.1.1 + dev: true - get-stream@4.1.0: + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} dependencies: - pump: 3.0.0 + is-extglob: 2.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.0 + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} - get-stream@6.0.1: {} + /is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + dev: true - get-symbol-description@1.0.0: + /is-language-code@3.1.0: + resolution: {integrity: sha512-zJdQ3QTeLye+iphMeK3wks+vXSRFKh68/Pnlw7aOfApFSEIOhYa8P9vwwa6QrImNNBMJTiL1PpYF0f4BxDuEgA==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - - get-value@2.0.6: {} - - git-hooks-list@1.0.3: {} - - git-repo-info@1.4.1: {} - - git-repo-info@2.1.1: {} + '@babel/runtime': 7.26.0 - git-repo-version@1.0.2: - dependencies: - git-repo-info: 1.4.1 + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} - glob-parent@5.1.2: + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} dependencies: - is-glob: 4.0.3 + has-tostringtag: 1.0.2 - glob-parent@6.0.2: + /is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} dependencies: - is-glob: 4.0.3 + kind-of: 3.2.2 - glob-to-regexp@0.4.1: {} + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + requiresBuild: true - glob@5.0.15: - dependencies: - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} - glob@7.2.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 + /is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + dev: false - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: false - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.6 - once: 1.4.0 + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true - glob@9.3.4: - dependencies: - fs.realpath: 1.0.0 - minimatch: 8.0.3 - minipass: 4.2.5 - path-scurry: 1.6.3 + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} - global-modules@1.0.0: - dependencies: - global-prefix: 1.0.2 - is-windows: 1.0.2 - resolve-dir: 1.0.1 + /is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + dev: true - global-prefix@1.0.2: + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} dependencies: - expand-tilde: 2.0.2 - homedir-polyfill: 1.0.3 - ini: 1.3.8 - is-windows: 1.0.2 - which: 1.3.1 + isobject: 3.0.1 - globals@11.12.0: {} + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - globals@13.20.0: + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} dependencies: - type-fest: 0.20.2 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 - globals@9.18.0: {} + /is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true - globalthis@1.0.3: + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} dependencies: - define-properties: 1.2.0 + call-bind: 1.0.7 - globalyzer@0.1.0: {} + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} - globby@10.0.0: - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - glob: 7.2.3 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true - globby@10.0.2: + /is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - glob: 7.2.3 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 + has-tostringtag: 1.0.2 - globby@11.1.0: + /is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 + better-path-resolve: 1.0.0 - globrex@0.1.2: {} + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 - gopd@1.0.1: + /is-type@0.0.1: + resolution: {integrity: sha512-YwJh/zBVrcJ90aAnPBM0CbHvm7lG9ao7lIFeqTZ1UQj4iFLpM5CikdaU+dGGesrMJwxLqPGmjjrUrQ6Kn3Zh+w==} dependencies: - get-intrinsic: 1.2.0 + core-util-is: 1.0.3 - got@9.6.0: + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} dependencies: - '@sindresorhus/is': 0.14.0 - '@szmarczak/http-timer': 1.1.2 - '@types/keyv': 3.1.4 - '@types/responselike': 1.0.0 - cacheable-request: 6.1.0 - decompress-response: 3.3.0 - duplexer3: 0.1.5 - get-stream: 4.1.0 - lowercase-keys: 1.0.1 - mimic-response: 1.0.1 - p-cancelable: 1.1.0 - to-readable-stream: 1.0.0 - url-parse-lax: 3.0.0 + which-typed-array: 1.1.15 - graceful-fs@4.2.10: {} + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - graceful-fs@4.2.11: {} + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} - graceful-readlink@1.0.1: {} + /is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + dev: true - grapheme-splitter@1.0.4: {} + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 - growly@1.3.0: {} + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} - handlebars@4.7.7: + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.17.4 + is-docker: 2.2.1 - has-ansi@2.0.0: - dependencies: - ansi-regex: 2.1.1 + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - has-ansi@3.0.0: - dependencies: - ansi-regex: 3.0.1 + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - has-bigints@1.0.2: {} + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - has-flag@3.0.0: {} + /isbinaryfile@5.0.4: + resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==} + engines: {node: '>= 18.0.0'} - has-flag@4.0.0: {} + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - has-property-descriptors@1.0.0: + /isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} dependencies: - get-intrinsic: 1.2.0 - - has-proto@1.0.1: {} + isarray: 1.0.0 - has-symbols@1.0.3: {} + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} - has-tostringtag@1.0.0: + /istextorbinary@2.1.0: + resolution: {integrity: sha512-kT1g2zxZ5Tdabtpp9VSdOzW9lb6LXImyWbzbQeTxoRtHhurC9Ej9Wckngr2+uepPL09ky/mJHmN9jeJPML5t6A==} + engines: {node: '>=0.12'} dependencies: - has-symbols: 1.0.3 + binaryextensions: 2.3.0 + editions: 1.3.4 + textextensions: 2.6.0 - has-unicode@2.0.1: {} + /istextorbinary@2.6.0: + resolution: {integrity: sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==} + engines: {node: '>=0.12'} + dependencies: + binaryextensions: 2.3.0 + editions: 2.3.1 + textextensions: 2.6.0 - has-value@0.3.1: + /jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} dependencies: - get-value: 2.0.6 - has-values: 0.1.4 - isobject: 2.1.0 + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true - has-value@1.0.0: + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} dependencies: - get-value: 2.0.6 - has-values: 1.0.0 - isobject: 3.0.1 + '@types/node': 20.17.6 + merge-stream: 2.0.0 + supports-color: 8.1.1 - has-values@0.1.4: {} + /jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + dev: false - has-values@1.0.0: - dependencies: - is-number: 3.0.0 - kind-of: 4.0.0 + /js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} - has@1.0.3: - dependencies: - function-bind: 1.1.1 + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - hash-for-dep@1.5.1: + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true dependencies: - broccoli-kitchen-sink-helpers: 0.3.1 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 - path-root: 0.1.1 - resolve: 1.22.1 - resolve-package-path: 1.2.7 - transitivePeerDependencies: - - supports-color + argparse: 1.0.10 + esprima: 4.0.1 - hawk@1.1.1: + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true dependencies: - boom: 0.4.2 - cryptiles: 0.2.2 - hoek: 0.9.1 - sntp: 0.2.4 - optional: true + argparse: 2.0.1 - he@1.2.0: {} + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: true - heimdalljs-fs-monitor@1.1.1: + /jsdom@19.0.0: + resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==} + engines: {node: '>=12'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true dependencies: - callsites: 3.1.0 - clean-stack: 2.2.0 - extract-stack: 2.0.0 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10 + abab: 2.0.6 + acorn: 8.14.0 + acorn-globals: 6.0.0 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.1 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 10.0.0 + ws: 8.18.0 + xml-name-validator: 4.0.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate + dev: true - heimdalljs-graph@1.0.0: {} - - heimdalljs-logger@0.1.10: + /jsdom@25.0.1(supports-color@8.1.1): + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true dependencies: - debug: 2.6.9 - heimdalljs: 0.2.6 + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.5(supports-color@8.1.1) + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.1 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate - heimdalljs@0.2.6: - dependencies: - rsvp: 3.2.1 + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true - highlight.js@10.7.3: {} + /json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + dev: true - hoek@0.9.1: - optional: true + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - homedir-polyfill@1.0.3: - dependencies: - parse-passwd: 1.0.0 + /json-fn@1.1.1: + resolution: {integrity: sha512-diGeurhgiazd1lfByjn83uQkF6fVFdiCiQgJyhN3/aCl7EKye0aZe3r9eeQPKcsCh81Mntrvt46z65cn7ZwZHA==} + dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - hosted-git-info@4.1.0: + /json-stable-stringify@1.1.1: + resolution: {integrity: sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==} + engines: {node: '>= 0.4'} dependencies: - lru-cache: 6.0.0 + call-bind: 1.0.7 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - hosted-git-info@6.1.1: + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true dependencies: - lru-cache: 7.18.3 + minimist: 1.2.8 + dev: false + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + /jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + optionalDependencies: + graceful-fs: 4.2.11 + + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 - html-encoding-sniffer@2.0.1: + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: - whatwg-encoding: 1.0.5 + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + /jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - html-encoding-sniffer@3.0.0: + /keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} dependencies: - whatwg-encoding: 2.0.0 + json-buffer: 3.0.0 + dev: true - http-cache-semantics@4.1.1: {} + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 - http-errors@1.6.3: + /kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 + is-buffer: 1.1.6 - http-errors@2.0.0: + /kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 + is-buffer: 1.1.6 + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + /klaw@1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true - http-parser-js@0.5.8: {} + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: false - http-proxy-agent@4.0.1: + /language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + dev: true + + /language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + language-subtag-registry: 0.3.23 + dev: true - http-proxy-agent@4.0.1(supports-color@8.1.1): + /lcid@3.1.1: + resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==} + engines: {node: '>=8'} dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2(supports-color@8.1.1) - debug: 4.3.4(supports-color@8.1.1) + invert-kv: 3.0.1 + + /lerna-changelog@2.2.0: + resolution: {integrity: sha512-yjYNAHrbnw8xYFKmYWJEP52Tk4xSdlNmzpYr26+3glbSGDmpe8UMo8f9DlEntjGufL+opup421oVTXcLshwAaQ==} + engines: {node: 12.* || 14.* || >= 16} + hasBin: true + dependencies: + chalk: 4.1.2 + cli-highlight: 2.1.11 + execa: 5.1.1 + hosted-git-info: 4.1.0 + make-fetch-happen: 9.1.0 + p-map: 3.0.0 + progress: 2.0.3 + yargs: 17.7.2 transitivePeerDependencies: + - bluebird - supports-color + dev: true - http-proxy-agent@5.0.0: + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + prelude-ls: 1.2.1 + type-check: 0.4.0 - http-proxy@1.18.1: + /line-column@1.0.2: + resolution: {integrity: sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==} dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.2 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug + isarray: 1.0.0 + isobject: 2.1.0 - http-proxy@1.18.1(debug@4.3.4): - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.2(debug@4.3.4) - requires-port: 1.0.0 - transitivePeerDependencies: - - debug + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - http-signature@0.10.1: + /linkify-it@1.2.4: + resolution: {integrity: sha512-eGHwtlABkp1NOJSiKUNqBf3SYAS5jPHtvRXPAgNaQwTqmkTahjtiLH9NtxdR5IOPhNvwNMN/diswSfZKzUkhGg==} dependencies: - asn1: 0.1.11 - assert-plus: 0.1.5 - ctype: 0.5.3 - optional: true + uc.micro: 1.0.6 + dev: true - https-proxy-agent@5.0.1: + /linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + uc.micro: 1.0.6 + + /livereload-js@3.4.1: + resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} - https-proxy-agent@5.0.1(supports-color@8.1.1): + /load-json-file@6.2.0: + resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} + engines: {node: '>=8'} dependencies: - agent-base: 6.0.2(supports-color@8.1.1) - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color + graceful-fs: 4.2.11 + parse-json: 5.2.0 + strip-bom: 4.0.0 + type-fest: 0.6.0 - https@1.0.0: {} + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} - human-signals@1.1.1: {} + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 - human-signals@2.1.0: {} + /loader.js@4.7.0: + resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} - humanize-ms@1.2.1: + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} dependencies: - ms: 2.1.3 + p-locate: 3.0.0 + path-exists: 3.0.0 - iconv-lite@0.4.24: + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} dependencies: - safer-buffer: 2.1.2 + p-locate: 4.1.0 - iconv-lite@0.6.3: + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} dependencies: - safer-buffer: 2.1.2 + p-locate: 5.0.0 - icss-utils@5.1.0(postcss@8.4.21): + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - postcss: 8.4.21 - - ieee754@1.2.1: {} - - ignore@5.2.4: {} + p-locate: 6.0.0 + dev: true - import-fresh@3.3.0: + /lodash._baseflatten@3.1.4: + resolution: {integrity: sha512-fESngZd+X4k+GbTxdMutf8ohQa0s3sJEHIcwtu4/LsIQ2JTDzdRxDCMQjW+ezzwRitLmHnacVVmosCbxifefbw==} dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + lodash.isarguments: 3.1.0 + lodash.isarray: 3.0.4 - imurmurhash@0.1.4: {} + /lodash._getnative@3.9.1: + resolution: {integrity: sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==} - indent-string@4.0.0: {} + /lodash._isiterateecall@3.0.9: + resolution: {integrity: sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==} - individual@3.0.0: {} + /lodash.assignin@4.2.0: + resolution: {integrity: sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==} - infer-owner@1.0.4: {} + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true - inflection@1.13.4: {} + /lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - inflection@2.0.1: {} + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - inflight@1.0.6: + /lodash.debounce@3.1.1: + resolution: {integrity: sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==} dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.3: {} + lodash._getnative: 3.9.1 - inherits@2.0.4: {} + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - ini@1.3.8: {} + /lodash.defaultsdeep@4.6.1: + resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==} + dev: true - ini@3.0.1: {} + /lodash.find@4.6.0: + resolution: {integrity: sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==} - inline-source-map-comment@1.0.5: + /lodash.flatten@3.0.2: + resolution: {integrity: sha512-jCXLoNcqQRbnT/KWZq2fIREHWeczrzpTR0vsycm96l/pu5hGeAntVBG0t7GuM/2wFqmnZs3d1eGptnAH2E8+xQ==} dependencies: - chalk: 1.1.3 - get-stdin: 4.0.1 - minimist: 1.2.8 - sum-up: 1.0.3 - xtend: 4.0.2 + lodash._baseflatten: 3.1.4 + lodash._isiterateecall: 3.0.9 - inquirer@6.5.2: - dependencies: - ansi-escapes: 3.2.0 - chalk: 2.4.2 - cli-cursor: 2.1.0 - cli-width: 2.2.1 - external-editor: 3.1.0 - figures: 2.0.0 - lodash: 4.17.21 - mute-stream: 0.0.7 - run-async: 2.4.1 - rxjs: 6.6.7 - string-width: 2.1.1 - strip-ansi: 5.2.0 - through: 2.3.8 + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false - inquirer@7.3.3: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - run-async: 2.4.1 - rxjs: 6.6.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - inquirer@8.2.5: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 7.0.0 + /lodash.isarray@3.0.4: + resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==} - internal-slot@1.0.5: - dependencies: - get-intrinsic: 1.2.0 - has: 1.0.3 - side-channel: 1.0.4 + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: true - invert-kv@3.0.1: {} + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - ip@2.0.0: {} + /lodash.omit@4.5.0: + resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} - ipaddr.js@1.9.1: {} + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - is-accessor-descriptor@0.1.6: - dependencies: - kind-of: 3.2.2 + /lodash.uniqby@4.7.0: + resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} - is-accessor-descriptor@1.0.0: - dependencies: - kind-of: 6.0.3 + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - is-array-buffer@3.0.2: + /log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - is-typed-array: 1.1.10 - - is-arrayish@0.2.1: {} + chalk: 2.4.2 - is-bigint@1.0.4: + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} dependencies: - has-bigints: 1.0.2 + chalk: 4.1.2 + is-unicode-supported: 0.1.0 - is-binary-path@2.1.0: + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: - binary-extensions: 2.2.0 + get-func-name: 2.0.2 + dev: true - is-boolean-object@1.1.2: + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 + tslib: 2.8.1 + dev: true - is-buffer@1.1.6: {} + /lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + dev: true - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: true - is-callable@1.2.7: {} + /lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - is-core-module@2.11.0: + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: - has: 1.0.3 + yallist: 3.1.1 - is-data-descriptor@0.1.4: + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} dependencies: - kind-of: 3.2.2 + yallist: 4.0.0 - is-data-descriptor@1.0.0: - dependencies: - kind-of: 6.0.3 + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} - is-date-object@1.0.5: + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: - has-tostringtag: 1.0.0 + sourcemap-codec: 1.4.8 - is-descriptor@0.1.6: + /magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} dependencies: - is-accessor-descriptor: 0.1.6 - is-data-descriptor: 0.1.4 - kind-of: 5.1.0 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: false - is-descriptor@1.0.2: + /make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} dependencies: - is-accessor-descriptor: 1.0.0 - is-data-descriptor: 1.0.0 - kind-of: 6.0.3 - - is-docker@2.2.1: {} + pify: 4.0.1 + semver: 5.7.2 + dev: false - is-extendable@0.1.1: {} + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 - is-extendable@1.0.1: + /make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} dependencies: - is-plain-object: 2.0.4 + agentkeepalive: 4.5.0 + cacache: 15.3.0 + http-cache-semantics: 4.1.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true - is-extglob@2.1.1: {} + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 - is-fullwidth-code-point@2.0.0: {} + /map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + dependencies: + p-defer: 1.0.0 - is-fullwidth-code-point@3.0.0: {} + /map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} - is-git-url@1.0.0: {} + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} - is-glob@3.1.0: + /map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} dependencies: - is-extglob: 2.1.1 + object-visit: 1.0.1 - is-glob@4.0.3: + /markdown-it-terminal@0.4.0(markdown-it@13.0.2): + resolution: {integrity: sha512-NeXtgpIK6jBciHTm9UhiPnyHDdqyVIdRPJ+KdQtZaf/wR74gvhCNbw5li4TYsxRp5u3ZoHEF4DwpECeZqyCw+w==} + peerDependencies: + markdown-it: '>= 13.0.0' dependencies: - is-extglob: 2.1.1 - - is-interactive@1.0.0: {} - - is-lambda@1.0.1: {} + ansi-styles: 3.2.1 + cardinal: 1.0.0 + cli-table: 0.3.11 + lodash.merge: 4.6.2 + markdown-it: 13.0.2 - is-language-code@3.1.0: + /markdown-it@13.0.2: + resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} + hasBin: true dependencies: - '@babel/runtime': 7.24.5 + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 - is-module@1.0.0: {} + /markdown-it@4.4.0: + resolution: {integrity: sha512-Rl8dHHeLuAh3E72OPY0tY7CLvlxgHiLhlshIYswAAabAg4YDBLa6e/LTgNkkxBO2K61ESzoquPQFMw/iMrT1PA==} + hasBin: true + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 1.2.4 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true - is-negative-zero@2.0.2: {} + /matcher-collection@1.1.2: + resolution: {integrity: sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==} + dependencies: + minimatch: 3.1.2 - is-number-object@1.0.7: + /matcher-collection@2.0.1: + resolution: {integrity: sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - has-tostringtag: 1.0.0 + '@types/minimatch': 3.0.5 + minimatch: 3.1.2 - is-number@3.0.0: + /md5-hex@3.0.1: + resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} + engines: {node: '>=8'} dependencies: - kind-of: 3.2.2 - - is-number@7.0.0: {} + blueimp-md5: 2.19.0 + dev: true - is-obj@2.0.0: {} + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: true - is-path-cwd@2.2.0: {} + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true - is-path-inside@3.0.3: {} + /mdn-links@0.1.0: + resolution: {integrity: sha512-m+gI2Hrgro1O0SwqHd9cFkqN8VGzP56eprB63gxu6z9EFQDMeaR083wcNqMVADIbgiMP/TOCCe0ZIXHLBv2tUg==} + dev: true - is-plain-obj@1.1.0: {} + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - is-plain-obj@2.1.0: {} + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} - is-plain-object@2.0.4: + /mem@5.1.1: + resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} + engines: {node: '>=8'} dependencies: - isobject: 3.0.1 - - is-potential-custom-element-name@1.0.1: {} + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 - is-regex@1.1.4: + /mem@8.1.1: + resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} + engines: {node: '>=10'} dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - - is-regexp@1.0.0: {} + map-age-cleaner: 0.1.3 + mimic-fn: 3.1.0 - is-shared-array-buffer@1.0.2: + /memory-streams@0.1.3: + resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} dependencies: - call-bind: 1.0.2 + readable-stream: 1.0.34 - is-stream@1.1.0: {} + /meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + dev: true - is-stream@2.0.1: {} + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - is-string@1.0.7: - dependencies: - has-tostringtag: 1.0.0 + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - is-subdir@1.2.0: + /merge-trees@2.0.0: + resolution: {integrity: sha512-5xBbmqYBalWqmhYm51XlohhkmVOua3VAUrrWh8t9iOkaLpS6ifqm/UVuUjQCeDVJ9Vx3g2l6ihfkbLSTeKsHbw==} dependencies: - better-path-resolve: 1.0.0 + fs-updater: 1.0.4 + heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color - is-symbol@1.0.4: - dependencies: - has-symbols: 1.0.3 + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} - is-type@0.0.1: - dependencies: - core-util-is: 1.0.3 + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} - is-typed-array@1.1.10: + /micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color - is-typedarray@1.0.0: {} + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 - is-unicode-supported@0.1.0: {} + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} - is-weakref@1.0.2: - dependencies: - call-bind: 1.0.2 + /mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} - is-windows@1.0.2: {} + /mime-types@1.0.2: + resolution: {integrity: sha512-echfutj/t5SoTL4WZpqjA1DCud1XO0WQF3/GJ48YBmc4ZMhCK77QA6Z/w6VTQERLKuJ4drze3kw2TUT8xZXVNw==} + engines: {node: '>= 0.8.0'} + dev: true - is-wsl@2.2.0: + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} dependencies: - is-docker: 2.2.1 + mime-db: 1.52.0 - isarray@0.0.1: {} + /mime@1.2.11: + resolution: {integrity: sha512-Ysa2F/nqTNGHhhm9MV8ure4+Hc+Y8AWiqUdHxsO7xu8zc92ND9f3kpALHjaP026Ft17UfxrMt95c50PLUeynBw==} + requiresBuild: true + dev: true + optional: true - isarray@1.0.0: {} + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true - isbinaryfile@5.0.0: {} + /mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} - isexe@2.0.0: {} + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} - isobject@2.1.0: - dependencies: - isarray: 1.0.0 + /mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} - isobject@3.0.1: {} + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true - istextorbinary@2.1.0: - dependencies: - binaryextensions: 2.3.0 - editions: 1.3.4 - textextensions: 2.6.0 + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true - istextorbinary@2.6.0: + /mini-css-extract-plugin@2.9.2(webpack@5.94.0): + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: 5.94.0 dependencies: - binaryextensions: 2.3.0 - editions: 2.3.1 - textextensions: 2.6.0 + schema-utils: 4.2.0 + tapable: 2.2.1 + webpack: 5.94.0 - jest-worker@27.5.1: + /minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} dependencies: - '@types/node': 18.15.10 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - js-sdsl@4.4.0: {} - - js-string-escape@1.0.1: {} - - js-tokens@3.0.2: {} - - js-tokens@4.0.0: {} + brace-expansion: 1.1.11 + dev: false - js-yaml@3.14.1: + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + brace-expansion: 1.1.11 - js-yaml@4.1.0: + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} dependencies: - argparse: 2.0.1 + brace-expansion: 2.0.1 - jsdom@16.7.0: + /minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} dependencies: - abab: 2.0.6 - acorn: 8.8.2 - acorn-globals: 6.0.0 - cssom: 0.4.4 - cssstyle: 2.3.0 - data-urls: 2.0.0 - decimal.js: 10.4.3 - domexception: 2.0.1 - escodegen: 2.0.0 - form-data: 3.0.1 - html-encoding-sniffer: 2.0.1 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 - parse5: 6.0.1 - saxes: 5.0.1 - symbol-tree: 3.2.4 - tough-cookie: 4.1.2 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 2.0.0 - webidl-conversions: 6.1.0 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 - ws: 7.5.9 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + brace-expansion: 2.0.1 - jsdom@16.7.0(supports-color@8.1.1): + /minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} dependencies: - abab: 2.0.6 - acorn: 8.8.2 - acorn-globals: 6.0.0 - cssom: 0.4.4 - cssstyle: 2.3.0 - data-urls: 2.0.0 - decimal.js: 10.4.3 - domexception: 2.0.1 - escodegen: 2.0.0 - form-data: 3.0.1 - html-encoding-sniffer: 2.0.1 - http-proxy-agent: 4.0.1(supports-color@8.1.1) - https-proxy-agent: 5.0.1(supports-color@8.1.1) - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 - parse5: 6.0.1 - saxes: 5.0.1 - symbol-tree: 3.2.4 - tough-cookie: 4.1.2 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 2.0.0 - webidl-conversions: 6.1.0 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 - ws: 7.5.9 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + brace-expansion: 2.0.1 - jsdom@19.0.0: + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} dependencies: - abab: 2.0.6 - acorn: 8.8.2 - acorn-globals: 6.0.0 - cssom: 0.5.0 - cssstyle: 2.3.0 - data-urls: 3.0.2 - decimal.js: 10.4.3 - domexception: 4.0.0 - escodegen: 2.0.0 - form-data: 4.0.0 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 - parse5: 6.0.1 - saxes: 5.0.1 - symbol-tree: 3.2.4 - tough-cookie: 4.1.2 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 3.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 10.0.0 - ws: 8.13.0 - xml-name-validator: 4.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsesc@0.3.0: {} - - jsesc@0.5.0: {} - - jsesc@2.5.2: {} - - json-buffer@3.0.0: {} - - json-fn@1.1.1: {} - - json-parse-better-errors@1.0.2: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} + brace-expansion: 2.0.1 - json-stable-stringify-without-jsonify@1.0.1: {} + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - json-stable-stringify@1.0.2: + /minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} dependencies: - jsonify: 0.0.1 + minipass: 3.3.6 + dev: true - json-stringify-safe@5.0.1: {} + /minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + dev: true - json-typescript@1.1.2: {} + /minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true - json5@0.5.1: {} + /minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + dependencies: + minipass: 3.3.6 + dev: true - json5@1.0.2: + /minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} dependencies: - minimist: 1.2.8 + minipass: 3.3.6 + dev: true - json5@2.2.3: {} + /minipass@2.9.0: + resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} + dependencies: + safe-buffer: 5.2.1 + yallist: 3.1.1 - jsonfile@2.4.0: - optionalDependencies: - graceful-fs: 4.2.11 + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: true - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 + /minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} - jsonfile@6.1.0: - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: true - jsonify@0.0.1: {} + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} - keyv@3.1.0: + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} dependencies: - json-buffer: 3.0.0 + minipass: 3.3.6 + yallist: 4.0.0 + dev: true - kind-of@3.2.2: + /mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} dependencies: - is-buffer: 1.1.6 + for-in: 1.0.2 + is-extendable: 1.0.1 - kind-of@4.0.0: + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true dependencies: - is-buffer: 1.1.6 + minimist: 1.2.8 - kind-of@5.1.0: {} + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true - kind-of@6.0.3: {} + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true - klaw@1.3.1: - optionalDependencies: - graceful-fs: 4.2.11 + /mktemp@0.4.0: + resolution: {integrity: sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==} + engines: {node: '>0.9'} - lcid@3.1.1: + /mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true dependencies: - invert-kv: 3.0.1 + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.3.7(supports-color@8.1.1) + diff: 5.2.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + dev: true - leek@0.0.24: + /morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} dependencies: + basic-auth: 2.0.1 debug: 2.6.9 - lodash.assign: 3.2.0 - rsvp: 3.6.2 - transitivePeerDependencies: - - supports-color - - lerna-changelog@2.2.0: - dependencies: - chalk: 4.1.2 - cli-highlight: 2.1.11 - execa: 5.1.1 - hosted-git-info: 4.1.0 - make-fetch-happen: 9.1.0 - p-map: 3.0.0 - progress: 2.0.3 - yargs: 17.7.1 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 transitivePeerDependencies: - - bluebird - supports-color - levn@0.3.0: - dependencies: - prelude-ls: 1.1.2 - type-check: 0.3.2 + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - line-column@1.0.2: - dependencies: - isarray: 1.0.0 - isobject: 2.1.0 + /muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + dev: false - lines-and-columns@1.2.4: {} + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true - linkify-it@1.2.4: - dependencies: - uc.micro: 1.0.6 + /mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} - linkify-it@4.0.1: - dependencies: - uc.micro: 1.0.6 + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - livereload-js@3.4.1: {} + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - load-json-file@6.2.0: + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: - graceful-fs: 4.2.11 - parse-json: 5.2.0 - strip-bom: 4.0.0 - type-fest: 0.6.0 + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true - loader-runner@4.3.0: {} + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true - loader-utils@2.0.4: + /nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - - loader.js@4.7.0: {} + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color - locate-path@2.0.0: - dependencies: - p-locate: 2.0.0 - path-exists: 3.0.0 + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - locate-path@3.0.0: + /ndjson@2.0.0: + resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} + engines: {node: '>=10'} + hasBin: true dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 + json-stringify-safe: 5.0.1 + minimist: 1.2.8 + readable-stream: 3.6.2 + split2: 3.2.2 + through2: 4.0.2 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 + /negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} - lodash._baseassign@3.2.0: - dependencies: - lodash._basecopy: 3.0.1 - lodash.keys: 3.1.2 + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - lodash._basecopy@3.0.1: {} + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - lodash._baseflatten@3.1.4: + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: - lodash.isarguments: 3.1.0 - lodash.isarray: 3.0.4 - - lodash._bindcallback@3.0.1: {} + lower-case: 2.0.2 + tslib: 2.8.1 + dev: true - lodash._createassigner@3.1.1: + /nock@13.5.6: + resolution: {integrity: sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==} + engines: {node: '>= 10.13'} dependencies: - lodash._bindcallback: 3.0.1 - lodash._isiterateecall: 3.0.9 - lodash.restparam: 3.6.1 + debug: 4.3.7(supports-color@8.1.1) + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true - lodash._getnative@3.9.1: {} + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true - lodash._isiterateecall@3.0.9: {} + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - lodash._reinterpolate@3.0.0: {} + /node-modules-path@1.0.2: + resolution: {integrity: sha512-6Gbjq+d7uhkO7epaKi5DNgUJn7H0gEyA4Jg0Mo1uQOi3Rk50G83LtmhhFyw0LxnAFhtlspkiiw52ISP13qzcBg==} + dev: true - lodash.assign@3.2.0: + /node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} dependencies: - lodash._baseassign: 3.2.0 - lodash._createassigner: 3.1.1 - lodash.keys: 3.1.2 + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.6.3 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 - lodash.assignin@4.2.0: {} + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - lodash.camelcase@4.3.0: {} + /node-uuid@1.4.8: + resolution: {integrity: sha512-TkCET/3rr9mUuRp+CpO7qfgT++aAxfDRaalQhwPFzI9BY/2rCDn6OfpZOVggi1AXfTPpfkTrg5f5WQx5G1uLxA==} + deprecated: Use uuid module instead + hasBin: true + dev: true - lodash.castarray@4.4.0: {} + /node-watch@0.7.3: + resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} + engines: {node: '>=6'} - lodash.clonedeep@4.5.0: {} + /nopt@3.0.6: + resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 - lodash.debounce@3.1.1: + /normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} dependencies: - lodash._getnative: 3.9.1 + remove-trailing-separator: 1.1.0 - lodash.debounce@4.0.8: {} + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} - lodash.defaultsdeep@4.6.1: {} + /normalize-registry-url@2.0.0: + resolution: {integrity: sha512-3e9FwDyRAhbxXw4slm4Tjv40u78yPwMc/WZkACpqNQOs5sM7wic853AeTLkMFEVhivZkclGYlse8iYsklz0Yvg==} - lodash.find@4.6.0: {} + /normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + dev: true - lodash.flatten@3.0.2: + /npm-package-arg@10.1.0: + resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - lodash._baseflatten: 3.1.4 - lodash._isiterateecall: 3.0.9 + hosted-git-info: 6.1.1 + proc-log: 3.0.0 + semver: 7.6.3 + validate-npm-package-name: 5.0.1 - lodash.foreach@4.5.0: {} + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 - lodash.isarguments@3.1.0: {} + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 - lodash.isarray@3.0.4: {} + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true - lodash.kebabcase@4.1.1: {} + /npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + dev: true - lodash.keys@3.1.2: + /npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. dependencies: - lodash._getnative: 3.9.1 - lodash.isarguments: 3.1.0 - lodash.isarray: 3.0.4 + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 - lodash.merge@4.6.2: {} + /npmlog@7.0.1: + resolution: {integrity: sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + dependencies: + are-we-there-yet: 4.0.2 + console-control-strings: 1.1.0 + gauge: 5.0.2 + set-blocking: 2.0.0 + dev: true - lodash.omit@4.5.0: {} + /nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} - lodash.restparam@3.6.1: {} + /oauth-sign@0.3.0: + resolution: {integrity: sha512-Tr31Sh5FnK9YKm7xTUPyDMsNOvMqkVDND0zvK/Wgj7/H9q8mpye0qG2nVzrnsvLhcsX5DtqXD0la0ks6rkPCGQ==} + requiresBuild: true + dev: true + optional: true - lodash.template@4.5.0: - dependencies: - lodash._reinterpolate: 3.0.0 - lodash.templatesettings: 4.2.0 + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} - lodash.templatesettings@4.2.0: + /object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} dependencies: - lodash._reinterpolate: 3.0.0 + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 - lodash.uniq@4.5.0: {} + /object-hash@1.3.1: + resolution: {integrity: sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==} + engines: {node: '>= 0.10.0'} - lodash.uniqby@4.7.0: {} + /object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} - lodash@4.17.21: {} + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} - log-symbols@2.2.0: + /object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} dependencies: - chalk: 2.4.2 + isobject: 3.0.1 - log-symbols@4.1.0: + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 - loose-envify@1.4.0: + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} dependencies: - js-tokens: 4.0.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 + dev: false - loupe@2.3.6: + /object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} dependencies: - get-func-name: 2.0.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + dev: false - lower-case@2.0.2: + /object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} dependencies: - tslib: 2.5.0 - - lowercase-keys@1.0.1: {} - - lowercase-keys@2.0.0: {} + isobject: 3.0.1 - lru-cache@5.1.1: + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} dependencies: - yallist: 3.1.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false - lru-cache@6.0.0: + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} dependencies: - yallist: 4.0.0 - - lru-cache@7.18.3: {} + ee-first: 1.1.1 - magic-string@0.25.9: + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} dependencies: - sourcemap-codec: 1.4.8 + ee-first: 1.1.1 - magic-string@0.30.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} - make-dir@2.1.0: + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: - pify: 4.0.1 - semver: 5.7.1 + wrappy: 1.0.2 - make-dir@3.1.0: + /onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} dependencies: - semver: 6.3.1 + mimic-fn: 1.2.0 - make-fetch-happen@9.1.0: + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} dependencies: - agentkeepalive: 4.3.0 - cacache: 15.3.0 - http-cache-semantics: 4.1.1 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 6.0.0 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-fetch: 1.4.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.3 - promise-retry: 2.0.1 - socks-proxy-agent: 6.2.1 - ssri: 8.0.1 - transitivePeerDependencies: - - bluebird - - supports-color + mimic-fn: 2.1.0 - makeerror@1.0.12: + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} dependencies: - tmpl: 1.0.5 + mimic-fn: 4.0.0 + dev: true - map-age-cleaner@0.1.3: + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} dependencies: - p-defer: 1.0.0 - - map-cache@0.2.2: {} - - map-obj@4.3.0: {} + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 - map-visit@1.0.0: + /ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} dependencies: - object-visit: 1.0.1 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 - markdown-it-terminal@0.4.0(markdown-it@13.0.1): + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} dependencies: - ansi-styles: 3.2.1 - cardinal: 1.0.0 - cli-table: 0.3.11 - lodash.merge: 4.6.2 - markdown-it: 13.0.1 + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 - markdown-it@13.0.1: - dependencies: - argparse: 2.0.1 - entities: 3.0.1 - linkify-it: 4.0.1 - mdurl: 1.0.1 - uc.micro: 1.0.6 + /os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + dev: true - markdown-it@4.4.0: + /os-locale@5.0.0: + resolution: {integrity: sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==} + engines: {node: '>=10'} dependencies: - argparse: 1.0.10 - entities: 1.1.2 - linkify-it: 1.2.4 - mdurl: 1.0.1 - uc.micro: 1.0.6 + execa: 4.1.0 + lcid: 3.1.1 + mem: 5.1.1 - matcher-collection@1.1.2: - dependencies: - minimatch: 3.1.2 + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} - matcher-collection@2.0.1: + /osenv@0.1.5: + resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + deprecated: This package is no longer supported. dependencies: - '@types/minimatch': 3.0.5 - minimatch: 3.1.2 + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + dev: true - md5-hex@3.0.1: - dependencies: - blueimp-md5: 2.19.0 + /p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + dev: true - mdn-data@2.0.14: {} + /p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} - mdn-data@2.0.30: {} + /p-defer@3.0.0: + resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} + engines: {node: '>=8'} - mdn-links@0.1.0: {} + /p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + dependencies: + p-map: 2.1.0 - mdurl@1.0.1: {} + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} - media-typer@0.3.0: {} + /p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} - mem@5.1.1: + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 2.1.0 - p-is-promise: 2.1.0 + p-try: 2.2.0 - mem@8.1.1: + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 + yocto-queue: 0.1.0 - memory-streams@0.1.3: + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - readable-stream: 1.0.34 + yocto-queue: 1.1.1 + dev: true - merge-descriptors@1.0.1: {} + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 - merge-stream@2.0.0: {} + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 - merge-trees@2.0.0: + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} dependencies: - fs-updater: 1.0.4 - heimdalljs: 0.2.6 - transitivePeerDependencies: - - supports-color + p-limit: 3.1.0 - merge2@1.4.1: {} + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true - methods@1.1.2: {} + /p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} - micromatch@3.1.10: + /p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} dependencies: - arr-diff: 4.0.0 - array-unique: 0.3.2 - braces: 2.3.2 - define-property: 2.0.2 - extend-shallow: 3.0.2 - extglob: 2.0.4 - fragment-cache: 0.2.1 - kind-of: 6.0.3 - nanomatch: 1.2.13 - object.pick: 1.3.0 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color + aggregate-error: 3.1.0 - micromatch@4.0.5: + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} dependencies: - braces: 3.0.2 - picomatch: 2.3.1 + aggregate-error: 3.1.0 + dev: true - mime-db@1.52.0: {} + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} - mime-types@1.0.2: {} + /package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + dev: true - mime-types@2.1.35: + /package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} dependencies: - mime-db: 1.52.0 - - mime@1.2.11: - optional: true + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.1 + dev: true - mime@1.6.0: {} + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 - mimic-fn@1.2.0: {} + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 - mimic-fn@2.1.0: {} + /parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} - mimic-fn@3.1.0: {} + /parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + dev: true - mimic-response@1.0.1: {} + /parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} - mini-css-extract-plugin@2.7.5(webpack@5.77.0): - dependencies: - schema-utils: 4.0.0 - webpack: 5.77.0 + /parse-static-imports@1.1.0: + resolution: {integrity: sha512-HlxrZcISCblEV0lzXmAHheH/8qEkKgmqkdxyHTPbSqsTUV8GzqmN1L+SSti+VbNPfbBO3bYLPHDiUs2avbAdbA==} - minimatch@3.1.2: + /parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} dependencies: - brace-expansion: 1.1.11 + parse5: 6.0.1 + dev: true - minimatch@5.0.1: - dependencies: - brace-expansion: 2.0.1 + /parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + dev: true - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.1 + /parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - minimatch@8.0.3: + /parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} dependencies: - brace-expansion: 2.0.1 + entities: 4.5.0 - minimist@0.2.4: {} + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} - minimist@1.2.8: {} + /pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} - minipass-collect@1.0.2: - dependencies: - minipass: 3.3.6 + /path-absolute@1.0.1: + resolution: {integrity: sha512-gds5iRhSeOcDtj8gfWkRHLtZKTPsFVuh7utbjYtvnclw4XM+ffRzJrwqMhOD1PVqef7nBLmgsu1vIujjvAJrAw==} + engines: {node: '>=4'} - minipass-fetch@1.4.1: - dependencies: - minipass: 3.3.6 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: false - minipass-flush@1.0.5: - dependencies: - minipass: 3.3.6 + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} - minipass-pipeline@1.2.4: - dependencies: - minipass: 3.3.6 + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} - minipass-sized@1.0.3: - dependencies: - minipass: 3.3.6 + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true - minipass@2.9.0: - dependencies: - safe-buffer: 5.2.1 - yallist: 3.1.1 + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} - minipass@3.3.6: - dependencies: - yallist: 4.0.0 + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} - minipass@4.2.5: {} + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true - mixin-deep@1.3.2: - dependencies: - for-in: 1.0.2 - is-extendable: 1.0.1 + /path-name@1.0.0: + resolution: {integrity: sha512-/dcAb5vMXH0f51yvMuSUqFpxUcA8JelbRmE5mW/p4CUJxrNgK24IkstnV7ENtg2IDGBOu6izKTG6eilbnbNKWQ==} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - mkdirp@1.0.4: {} + /path-posix@1.0.0: + resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==} - mktemp@0.4.0: {} + /path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} - mocha@10.2.0: + /path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} dependencies: - ansi-colors: 4.1.1 - browser-stdout: 1.3.1 - chokidar: 3.5.3 - debug: 4.3.4(supports-color@8.1.1) - diff: 5.0.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 7.2.0 - he: 1.2.0 - js-yaml: 4.1.0 - log-symbols: 4.1.0 - minimatch: 5.0.1 - ms: 2.1.3 - nanoid: 3.3.3 - serialize-javascript: 6.0.0 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.2.1 - yargs: 16.2.0 - yargs-parser: 20.2.4 - yargs-unparser: 2.0.0 + path-root-regex: 0.1.2 - morgan@1.10.0: + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} dependencies: - basic-auth: 2.0.1 - debug: 2.6.9 - depd: 2.0.0 - on-finished: 2.3.0 - on-headers: 1.0.2 - transitivePeerDependencies: - - supports-color + lru-cache: 10.4.3 + minipass: 7.1.2 - mout@1.2.4: {} - - ms@2.0.0: {} + /path-temp@2.1.0: + resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} + engines: {node: '>=8.15'} + dependencies: + unique-string: 2.0.0 - ms@2.1.2: {} + /path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} - ms@2.1.3: {} + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} - mustache@4.2.0: {} + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: true - mute-stream@0.0.7: {} + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true - mute-stream@0.0.8: {} + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} - nanoid@3.3.3: {} + /picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} - nanoid@3.3.6: {} + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: false - nanomatch@1.2.13: + /pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} dependencies: - arr-diff: 4.0.0 - array-unique: 0.3.2 - define-property: 2.0.2 - extend-shallow: 3.0.2 - fragment-cache: 0.2.1 - is-windows: 1.0.2 - kind-of: 6.0.3 - object.pick: 1.3.0 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - - natural-compare-lite@1.4.0: {} + pinkie: 2.0.4 + dev: true - natural-compare@1.4.0: {} + /pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + dev: true - ndjson@2.0.0: + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} dependencies: - json-stringify-safe: 5.0.1 - minimist: 1.2.8 - readable-stream: 3.6.2 - split2: 3.2.2 - through2: 4.0.2 + find-up: 4.1.0 - negotiator@0.6.3: {} + /pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + dependencies: + find-up: 6.3.0 + dev: true - neo-async@2.6.2: {} + /pkg-entry-points@1.1.1: + resolution: {integrity: sha512-BhZa7iaPmB4b3vKIACoppyUoYn8/sFs17VJJtzrzPZvEnN2nqrgg911tdL65lA2m1ml6UI3iPeYbZQ4VXpn1mA==} - nice-try@1.0.5: {} + /pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 - no-case@3.0.4: + /pnpm-sync-dependencies-meta-injected@0.0.14: + resolution: {integrity: sha512-LcP6OFRvYWwYjyRSchOUoz/xzIzTNpIEDx3euL8EY5j6MAG5BVl3ZRQETSSoo/lZkUOoxhpysCYMhBaFjsW+cw==} + engines: {node: '>=16.0.0'} + hasBin: true dependencies: - lower-case: 2.0.2 - tslib: 2.5.0 + '@pnpm/find-workspace-dir': 6.0.3 + '@pnpm/find-workspace-packages': 6.0.9(@pnpm/logger@5.2.0) + '@pnpm/fs.hard-link-dir': 2.0.1(@pnpm/logger@5.2.0) + '@pnpm/logger': 5.2.0 + '@pnpm/read-project-manifest': 5.0.11 + debug: 4.3.7(supports-color@9.4.0) + fs-extra: 11.2.0 + proper-lockfile: 4.1.2 + resolve-package-path: 4.0.3 + supports-color: 9.4.0 + watcher: 2.3.1 + yargs: 17.7.2 - nock@13.3.0: + /portfinder@1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} dependencies: - debug: 4.3.4 - json-stringify-safe: 5.0.1 - lodash: 4.17.21 - propagate: 2.0.1 + async: 2.6.4 + debug: 3.2.7 + mkdirp: 0.5.6 transitivePeerDependencies: - supports-color - node-fetch@2.6.9: - dependencies: - whatwg-url: 5.0.0 - - node-int64@0.4.0: {} + /posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} - node-modules-path@1.0.2: {} + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} - node-notifier@10.0.1: + /postcss-modules-extract-imports@3.1.0(postcss@8.4.49): + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 dependencies: - growly: 1.3.0 - is-wsl: 2.2.0 - semver: 7.6.0 - shellwords: 0.1.1 - uuid: 8.3.2 - which: 2.0.2 - - node-releases@2.0.10: {} + postcss: 8.4.49 - node-releases@2.0.14: {} - - node-uuid@1.4.8: {} + /postcss-modules-local-by-default@4.1.0(postcss@8.4.49): + resolution: {integrity: sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-selector-parser: 7.0.0 + postcss-value-parser: 4.2.0 - node-watch@0.7.3: {} + /postcss-modules-scope@3.2.1(postcss@8.4.49): + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 7.0.0 - nopt@3.0.6: + /postcss-modules-values@4.0.0(postcss@8.4.49): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 dependencies: - abbrev: 1.1.1 + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 - normalize-path@2.1.1: + /postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} dependencies: - remove-trailing-separator: 1.1.0 + cssesc: 3.0.0 + util-deprecate: 1.0.2 - normalize-path@3.0.0: {} + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - normalize-registry-url@2.0.0: {} + /postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 - normalize-url@4.5.1: {} + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} - npm-git-info@1.0.3: {} + /prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + dev: true - npm-package-arg@10.1.0: + /pretender@3.4.7: + resolution: {integrity: sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw==} dependencies: - hosted-git-info: 6.1.1 - proc-log: 3.0.0 - semver: 7.6.0 - validate-npm-package-name: 5.0.0 + fake-xml-http-request: 2.1.2 + route-recognizer: 0.3.4 + dev: true - npm-run-path@2.0.2: + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} dependencies: - path-key: 2.0.1 + fast-diff: 1.3.0 + dev: true - npm-run-path@3.1.0: + /prettier-plugin-ember-template-tag@2.0.4(prettier@3.3.3): + resolution: {integrity: sha512-Ude3MJyPBMr/Er5aSS9Y0dsnHWX3prpJB+Jj/BKKUT/EvG2ftnIMBsZXmRu68RJA62JJB8MdKBloYmCu2pTRNg==} + engines: {node: 18.* || >= 20} + peerDependencies: + prettier: '>= 3.0.0' dependencies: - path-key: 3.1.1 + '@babel/core': 7.26.0 + content-tag: 2.0.3 + prettier: 3.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + /prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} - npm-run-path@4.0.1: + /pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} dependencies: - path-key: 3.1.1 + parse-ms: 2.1.0 - npmlog@6.0.2: + /pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} dependencies: - are-we-there-yet: 3.0.1 - console-control-strings: 1.1.0 - gauge: 4.0.4 - set-blocking: 2.0.0 + parse-ms: 4.0.0 + dev: true - nwsapi@2.2.2: {} + /printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - oauth-sign@0.3.0: - optional: true + /printf@0.6.1: + resolution: {integrity: sha512-is0ctgGdPJ5951KulgfzvHGwJtZ5ck8l042vRkV6jrkpBzTmb/lueTqguWHy2JfVA+RY6gFVlaZgUS0j7S/dsw==} + engines: {node: '>= 0.9.0'} - object-assign@4.1.1: {} + /private@0.1.8: + resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} + engines: {node: '>= 0.6'} - object-copy@0.1.0: - dependencies: - copy-descriptor: 0.1.1 - define-property: 0.2.5 - kind-of: 3.2.2 + /proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - object-hash@1.3.1: {} + /process-relative-require@1.0.0: + resolution: {integrity: sha512-r8G5WJPozMJAiv8sDdVWKgJ4In/zBXqwJdMCGAXQt2Kd3HdbAuJVzWYM4JW150hWoaI9DjhtbjcsCCHIMxm8RA==} + dependencies: + node-modules-path: 1.0.2 + dev: true - object-inspect@1.12.3: {} + /progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + dev: true - object-keys@1.1.1: {} + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: true - object-visit@1.0.1: + /promise-make-counter@1.0.1: + resolution: {integrity: sha512-R1JGFIgSJDpNV/JXxytAx6K79noEpcBiZXWDa3ic9WEMpBZbUdVVQjlA266SCicJ9CGqd70iGbbzbjRKbGU1Jg==} dependencies: - isobject: 3.0.1 + promise-make-naked: 3.0.0 - object.assign@4.1.4: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 - object-keys: 1.1.1 + /promise-make-naked@3.0.0: + resolution: {integrity: sha512-h71wwAMB2udFnlPmcxQMqKl6CckNLVKdk/ROtFivE6/VmW+rQKV0DWlGJ6VphRIoq22Tkonvdi3F+jlm5XDlow==} - object.pick@1.3.0: + /promise-map-series@0.2.3: + resolution: {integrity: sha512-wx9Chrutvqu1N/NHzTayZjE1BgIwt6SJykQoCOic4IZ9yUDjKyVYrpLa/4YCNsV61eRENfs29hrEquVuB13Zlw==} dependencies: - isobject: 3.0.1 + rsvp: 3.6.2 - object.values@1.1.6: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 + /promise-map-series@0.3.0: + resolution: {integrity: sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==} + engines: {node: 10.* || >= 12.*} - on-finished@2.3.0: + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} dependencies: - ee-first: 1.1.1 + err-code: 2.0.3 + retry: 0.12.0 + dev: true + + /promise.hash.helper@1.0.8: + resolution: {integrity: sha512-KYcnXctWUWyVD3W3Ye0ZDuA1N8Szrh85cVCxpG6xYrOk/0CttRtYCmU30nWsUch0NuExQQ63QXvzRE6FLimZmg==} + engines: {node: 10.* || >= 12.*} - on-finished@2.4.1: + /promise.prototype.finally@3.1.8: + resolution: {integrity: sha512-aVDtsXOml9iuMJzUco9J1je/UrIT3oMYfWkCTiUhkt+AvZw72q4dUZnR/R/eB3h5GeAagQVXvM1ApoYniJiwoA==} + engines: {node: '>= 0.4'} dependencies: - ee-first: 1.1.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true - on-headers@1.0.2: {} + /propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + dev: true - once@1.4.0: + /proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} dependencies: - wrappy: 1.0.2 + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 - onetime@2.0.1: - dependencies: - mimic-fn: 1.2.0 + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - onetime@5.1.2: + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} dependencies: - mimic-fn: 2.1.0 + forwarded: 0.2.0 + ipaddr.js: 1.9.1 - optionator@0.8.3: + /psl@1.10.0: + resolution: {integrity: sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==} dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.3.0 - prelude-ls: 1.1.2 - type-check: 0.3.2 - word-wrap: 1.2.3 + punycode: 2.3.1 + dev: true - optionator@0.9.1: + /pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 + end-of-stream: 1.4.4 + once: 1.4.0 - ora@3.4.0: - dependencies: - chalk: 2.4.2 - cli-cursor: 2.1.0 - cli-spinners: 2.7.0 - log-symbols: 2.2.0 - strip-ansi: 5.2.0 - wcwidth: 1.0.1 + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: true - ora@5.4.1: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.7.0 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} - os-homedir@1.0.2: {} + /qs@1.0.2: + resolution: {integrity: sha512-tHuOP9TN/1VmDM/ylApGK1QF3PSIP8I6bHDEfoKNQeViREQ/sfu1bAUrA1hoDun8p8Tpm7jcsz47g+3PiGoYdg==} + dev: true - os-locale@5.0.0: + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} dependencies: - execa: 4.1.0 - lcid: 3.1.1 - mem: 5.1.1 + side-channel: 1.0.6 - os-tmpdir@1.0.2: {} + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true - osenv@0.1.5: - dependencies: - os-homedir: 1.0.2 - os-tmpdir: 1.0.2 + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - p-cancelable@1.1.0: {} + /quibble@0.9.2: + resolution: {integrity: sha512-BrL7hrZcbyyt5ZDfePkGFDc3m82uUtxCPOnpRUrkOdtBnmV9ldQKxXORkKL8eIzToRNaCpIPyKyfdfq/tBlFAA==} + engines: {node: '>= 0.14.0'} + dependencies: + lodash: 4.17.21 + resolve: 1.22.8 + dev: true - p-defer@1.0.0: {} + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} - p-defer@3.0.0: {} + /quick-temp@0.1.8: + resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==} + dependencies: + mktemp: 0.4.0 + rimraf: 2.7.1 + underscore.string: 3.3.6 - p-filter@2.1.0: + /qunit-dom@3.3.0: + resolution: {integrity: sha512-sGPhNbZ/3gPbH3rp7PxnzqXuh6cyN/AG/vO/X9k0IYlsoBLi83MU6PdtSw/KiWLV8X/32jxD6fbeP9vjNAY4Dw==} dependencies: - p-map: 2.1.0 + dom-element-descriptors: 0.5.1 + dev: true - p-finally@1.0.0: {} + /qunit-theme-ember@1.0.0: + resolution: {integrity: sha512-vdMVVo6ecdCkWttMTKeyq1ZTLGHcA6zdze2zhguNuc3ritlJMhOXY5RDseqazOwqZVfCg3rtlmL3fMUyIzUyFQ==} + dev: true - p-finally@2.0.1: {} + /qunit@2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu): + resolution: {integrity: sha512-aqUzzUeCqlleWYKlpgfdHHw9C6KxkB9H3wNfiBg5yHqQMzy0xw/pbCRHYFkjl8MsP/t8qkTQE+JTYL71azgiew==} + engines: {node: '>=10'} + hasBin: true + dependencies: + commander: 7.2.0 + node-watch: 0.7.3 + tiny-glob: 0.2.9 + patched: true - p-is-promise@2.1.0: {} + /rambda@7.5.0: + resolution: {integrity: sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==} + dev: false - p-limit@1.3.0: + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: - p-try: 1.0.0 + safe-buffer: 5.2.1 - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} - p-limit@3.1.0: + /raw-body@1.1.7: + resolution: {integrity: sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==} + engines: {node: '>= 0.8.0'} dependencies: - yocto-queue: 0.1.0 + bytes: 1.0.0 + string_decoder: 0.10.31 - p-locate@2.0.0: + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} dependencies: - p-limit: 1.3.0 + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 - p-locate@3.0.0: + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true dependencies: - p-limit: 2.3.0 + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true - p-locate@4.1.0: + /read-ini-file@4.0.0: + resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==} + engines: {node: '>=14.6'} dependencies: - p-limit: 2.3.0 + ini: 3.0.1 + strip-bom: 4.0.0 - p-locate@5.0.0: + /read-yaml-file@2.1.0: + resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} + engines: {node: '>=10.13'} dependencies: - p-limit: 3.1.0 + js-yaml: 4.1.0 + strip-bom: 4.0.0 - p-map@2.1.0: {} + /readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 - p-map@3.0.0: + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} dependencies: - aggregate-error: 3.1.0 + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 - p-map@4.0.0: + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + requiresBuild: true dependencies: - aggregate-error: 3.1.0 - - p-try@1.0.0: {} + picomatch: 2.3.1 - p-try@2.2.0: {} + /realpath-missing@1.1.0: + resolution: {integrity: sha512-wnWtnywepjg/eHIgWR97R7UuM5i+qHLA195qdN9UPKvcMqfn60+67S8sPPW3vDlSEfYHoFkKU8IvpCNty3zQvQ==} + engines: {node: '>=10'} - package-json@6.5.0: + /recast@0.18.10: + resolution: {integrity: sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==} + engines: {node: '>= 4'} dependencies: - got: 9.6.0 - registry-auth-token: 4.2.2 - registry-url: 5.1.0 - semver: 6.3.1 + ast-types: 0.13.3 + esprima: 4.0.1 + private: 0.1.8 + source-map: 0.6.1 - parent-module@1.0.1: + /recast@0.19.1: + resolution: {integrity: sha512-8FCjrBxjeEU2O6I+2hyHyBFH1siJbMBLwIRvVr1T3FD2cL754sOaJDsJ/8h3xYltasbJ8jqWRIhMuDGBSiSbjw==} + engines: {node: '>= 4'} dependencies: - callsites: 3.1.0 + ast-types: 0.13.3 + esprima: 4.0.1 + private: 0.1.8 + source-map: 0.6.1 + dev: true - parse-json@5.2.0: + /redeyed@1.0.1: + resolution: {integrity: sha512-8eEWsNCkV2rvwKLS1Cvp5agNjMhwRe2um+y32B2+3LqOzg4C9BBPs6vzAfV16Ivb8B9HPNKIqd8OrdBws8kNlQ==} dependencies: - '@babel/code-frame': 7.24.2 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse-ms@2.1.0: {} - - parse-passwd@1.0.0: {} - - parse-static-imports@1.1.0: {} + esprima: 3.0.0 - parse5-htmlparser2-tree-adapter@6.0.1: + /regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} dependencies: - parse5: 6.0.1 - - parse5@5.1.1: {} - - parse5@6.0.1: {} - - parseurl@1.3.3: {} - - pascalcase@0.1.1: {} - - path-absolute@1.0.1: {} - - path-exists@3.0.0: {} - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@2.0.1: {} - - path-key@3.1.1: {} + regenerate: 1.4.2 - path-name@1.0.0: {} + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - path-parse@1.0.7: {} + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - path-posix@1.0.0: {} + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - path-root-regex@0.1.2: {} + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.26.0 - path-root@0.1.1: + /regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} dependencies: - path-root-regex: 0.1.2 + extend-shallow: 3.0.2 + safe-regex: 1.1.0 - path-scurry@1.6.3: + /regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} dependencies: - lru-cache: 7.18.3 - minipass: 4.2.5 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 - path-temp@2.1.0: + /regexpu-core@6.1.1: + resolution: {integrity: sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==} + engines: {node: '>=4'} dependencies: - unique-string: 2.0.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.11.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 - path-to-regexp@0.1.7: {} + /registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + dependencies: + rc: 1.2.8 + dev: true - path-type@4.0.0: {} + /registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + dependencies: + rc: 1.2.8 + dev: true - pathval@1.1.1: {} + /regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - picocolors@1.0.0: {} + /regjsparser@0.11.2: + resolution: {integrity: sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==} + hasBin: true + dependencies: + jsesc: 3.0.2 - picomatch@2.3.1: {} + /remote-git-tags@3.0.0: + resolution: {integrity: sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==} + engines: {node: '>=8'} + dev: true - pify@4.0.1: {} + /remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} - pinkie-promise@2.0.1: + /remove-types@1.0.0: + resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} dependencies: - pinkie: 2.0.4 + '@babel/core': 7.26.0 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) + prettier: 2.8.8 + transitivePeerDependencies: + - supports-color - pinkie@2.0.4: {} + /repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} - pkg-up@2.0.0: + /request@2.40.0: + resolution: {integrity: sha512-waNoGB4Z7bPn+lgqPk7l7hhze4Vd68jKccnwLeS7vr9GMxz0iWQbYTbBNWzfIk87Urx7V44pu29qjF/omej+Fw==} + engines: {'0': node >= 0.8.0} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 dependencies: - find-up: 2.1.0 + forever-agent: 0.5.2 + json-stringify-safe: 5.0.1 + mime-types: 1.0.2 + node-uuid: 1.4.8 + qs: 1.0.2 + optionalDependencies: + aws-sign2: 0.5.0 + form-data: 0.1.4 + hawk: 1.1.1 + http-signature: 0.10.1 + oauth-sign: 0.3.0 + stringstream: 0.0.6 + tough-cookie: 5.0.0 + tunnel-agent: 0.4.3 + dev: true - pkg-up@3.1.0: - dependencies: - find-up: 3.0.0 + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} - pnpm-sync-dependencies-meta-injected@0.0.10: - dependencies: - '@pnpm/find-workspace-dir': 6.0.3 - '@pnpm/find-workspace-packages': 6.0.9(@pnpm/logger@5.0.0) - '@pnpm/fs.hard-link-dir': 2.0.1(@pnpm/logger@5.0.0) - '@pnpm/logger': 5.0.0 - '@pnpm/read-project-manifest': 5.0.11 - debug: 4.3.4(supports-color@9.4.0) - fs-extra: 11.2.0 - proper-lockfile: 4.1.2 - resolve-package-path: 4.0.3 - supports-color: 9.4.0 - watcher: 2.3.1 - yargs: 17.7.2 + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} - portfinder@1.0.32: - dependencies: - async: 2.6.4 - debug: 3.2.7 - mkdirp: 0.5.6 - transitivePeerDependencies: - - supports-color + /requireindex@1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} - posix-character-classes@0.1.1: {} + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - postcss-modules-extract-imports@3.0.0(postcss@8.4.21): - dependencies: - postcss: 8.4.21 + /reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} - postcss-modules-local-by-default@4.0.0(postcss@8.4.21): + /resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} dependencies: - icss-utils: 5.1.0(postcss@8.4.21) - postcss: 8.4.21 - postcss-selector-parser: 6.0.11 - postcss-value-parser: 4.2.0 + expand-tilde: 2.0.2 + global-modules: 1.0.0 - postcss-modules-scope@3.0.0(postcss@8.4.21): - dependencies: - postcss: 8.4.21 - postcss-selector-parser: 6.0.11 + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} - postcss-modules-values@4.0.0(postcss@8.4.21): + /resolve-package-path@1.2.7: + resolution: {integrity: sha512-fVEKHGeK85bGbVFuwO9o1aU0n3vqQGrezPc51JGu9UTXpFQfWq5qCeKxyaRUSvephs+06c5j5rPq/dzHGEo8+Q==} dependencies: - icss-utils: 5.1.0(postcss@8.4.21) - postcss: 8.4.21 + path-root: 0.1.1 + resolve: 1.22.8 - postcss-selector-parser@6.0.11: + /resolve-package-path@3.1.0: + resolution: {integrity: sha512-2oC2EjWbMJwvSN6Z7DbDfJMnD8MYEouaLn5eIX0j8XwPsYCVIyY9bbnX88YHVkbr8XHqvZrYbxaLPibfTYKZMA==} + engines: {node: 10.* || >= 12} dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 + path-root: 0.1.1 + resolve: 1.22.8 - postcss-value-parser@4.2.0: {} + /resolve-package-path@4.0.3: + resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} + engines: {node: '>= 12'} + dependencies: + path-root: 0.1.1 - postcss@8.4.21: + /resolve-path@1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 + http-errors: 1.6.3 + path-is-absolute: 1.0.1 - prelude-ls@1.1.2: {} + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - prelude-ls@1.2.1: {} + /resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated - prepend-http@2.0.0: {} + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true - pretender@3.4.7: + /resolve@1.19.0: + resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} dependencies: - fake-xml-http-request: 2.1.2 - route-recognizer: 0.3.4 + is-core-module: 2.15.1 + path-parse: 1.0.7 + dev: false - prettier-linter-helpers@1.0.0: + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true dependencies: - fast-diff: 1.2.0 + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 - prettier@2.8.7: {} + /responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + dependencies: + lowercase-keys: 1.0.1 + dev: true - pretty-bytes@5.6.0: {} + /restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 - pretty-ms@7.0.1: + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} dependencies: - parse-ms: 2.1.0 + onetime: 5.1.2 + signal-exit: 3.0.7 - printable-characters@1.0.42: {} + /ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} - printf@0.6.1: {} + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} - private@0.1.8: {} + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - proc-log@3.0.0: {} + /right-pad@1.0.1: + resolution: {integrity: sha512-bYBjgxmkvTAfgIYy328fmkwhp39v8lwVgWhhrzxPV3yHtcSqyYKe9/XOhvW48UFjATg3VuJbpsp5822ACNvkmw==} + engines: {node: '>= 0.10'} + deprecated: Please use String.prototype.padEnd() over this package. - process-relative-require@1.0.0: + /rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true dependencies: - node-modules-path: 1.0.2 - - progress@2.0.3: {} - - promise-inflight@1.0.1: {} - - promise-make-naked@2.1.2: {} + glob: 7.2.3 - promise-map-series@0.2.3: + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true dependencies: - rsvp: 3.6.2 - - promise-map-series@0.3.0: {} + glob: 7.2.3 - promise-retry@2.0.1: + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true dependencies: - err-code: 2.0.3 - retry: 0.12.0 - - promise.hash.helper@1.0.8: {} + glob: 7.2.3 - promise.prototype.finally@3.1.4: + /rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - - propagate@2.0.1: {} + glob: 10.4.5 + dev: true - proper-lockfile@4.1.2: + /rollup-plugin-copy-assets@2.0.3(rollup@4.25.0): + resolution: {integrity: sha512-ETShhQGb9SoiwcNrvb3BhUNSGR89Jao0+XxxfzzLW1YsUzx8+rMO4z9oqWWmo6OHUmfNQRvqRj0cAyPkS9lN9w==} + peerDependencies: + rollup: '>=1.1.2' dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - - proto-list@1.2.4: {} + fs-extra: 7.0.1 + rollup: 4.25.0 + dev: false - proxy-addr@2.0.7: + /rollup-plugin-delete@2.1.0(rollup@4.25.0): + resolution: {integrity: sha512-TEbqJd7giLvzQDTu4jSPufwhTJs/iYVN2LfR/YIYkqjC/oZ0/h9Q0AeljifIhzBzJYZtHQTWKEbMms5fbh54pw==} + engines: {node: '>=10'} + peerDependencies: + rollup: '*' dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - psl@1.9.0: {} + del: 5.1.0 + rollup: 4.25.0 + dev: false - pump@3.0.0: + /rollup@4.25.0: + resolution: {integrity: sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - - punycode@1.3.2: {} - - punycode@2.3.0: {} - - qs@1.0.2: {} + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.25.0 + '@rollup/rollup-android-arm64': 4.25.0 + '@rollup/rollup-darwin-arm64': 4.25.0 + '@rollup/rollup-darwin-x64': 4.25.0 + '@rollup/rollup-freebsd-arm64': 4.25.0 + '@rollup/rollup-freebsd-x64': 4.25.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.25.0 + '@rollup/rollup-linux-arm-musleabihf': 4.25.0 + '@rollup/rollup-linux-arm64-gnu': 4.25.0 + '@rollup/rollup-linux-arm64-musl': 4.25.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.25.0 + '@rollup/rollup-linux-riscv64-gnu': 4.25.0 + '@rollup/rollup-linux-s390x-gnu': 4.25.0 + '@rollup/rollup-linux-x64-gnu': 4.25.0 + '@rollup/rollup-linux-x64-musl': 4.25.0 + '@rollup/rollup-win32-arm64-msvc': 4.25.0 + '@rollup/rollup-win32-ia32-msvc': 4.25.0 + '@rollup/rollup-win32-x64-msvc': 4.25.0 + fsevents: 2.3.3 + + /route-recognizer@0.3.4: + resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} - qs@6.11.0: + /router_js@8.0.6(route-recognizer@0.3.4): + resolution: {integrity: sha512-AjGxRDIpTGoAG8admFmvP/cxn1AlwwuosCclMU4R5oGHGt7ER0XtB3l9O04ToBDdPe4ivM/YcLopgBEpJssJ/Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + route-recognizer: ^0.3.4 + rsvp: ^4.8.5 + peerDependenciesMeta: + rsvp: + optional: true dependencies: - side-channel: 1.0.4 + '@glimmer/env': 0.1.7 + route-recognizer: 0.3.4 - qs@6.11.1: - dependencies: - side-channel: 1.0.4 + /rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} - querystring@0.2.0: {} + /rsvp@3.2.1: + resolution: {integrity: sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==} - querystringify@2.2.0: {} + /rsvp@3.6.2: + resolution: {integrity: sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==} + engines: {node: 0.12.* || 4.* || 6.* || >= 7.*} - queue-microtask@1.2.3: {} + /rsvp@4.8.5: + resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} + engines: {node: 6.* || >= 7.*} - quibble@0.6.17: - dependencies: - lodash: 4.17.21 - resolve: 1.22.1 + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} - quick-lru@4.0.1: {} + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} - quick-temp@0.1.8: + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: - mktemp: 0.4.0 - rimraf: 2.7.1 - underscore.string: 3.3.6 + queue-microtask: 1.2.3 - qunit-console-grouper@0.3.0: + /rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} dependencies: - broccoli-funnel: 3.0.8 - transitivePeerDependencies: - - supports-color + tslib: 1.14.1 - qunit-dom@2.0.0: + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - transitivePeerDependencies: - - supports-color + tslib: 2.8.1 - qunit@2.19.4: + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} dependencies: - commander: 7.2.0 - node-watch: 0.7.3 - tiny-glob: 0.2.9 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 - rambda@7.5.0: {} + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - randombytes@2.1.0: + /safe-execa@0.1.2: + resolution: {integrity: sha512-vdTshSQ2JsRCgT8eKZWNJIL26C6bVqy1SOmuCMlKHegVeo8KYRobRrefOdUq9OozSPUUiSxrylteeRmLOMFfWg==} + engines: {node: '>=12'} dependencies: - safe-buffer: 5.2.1 + '@zkochan/which': 2.0.3 + execa: 5.1.1 + path-name: 1.0.0 - range-parser@1.2.1: {} + /safe-json-parse@1.0.1: + resolution: {integrity: sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==} - raw-body@1.1.7: + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} dependencies: - bytes: 1.0.0 - string_decoder: 0.10.31 + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 - raw-body@2.5.1: + /safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 + ret: 0.1.15 + + /safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - raw-body@2.5.2: + /sane@4.1.0: + resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added + hasBin: true dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 + '@cnakazawa/watch': 1.0.4 + anymatch: 2.0.0 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 1.0.0 + fb-watchman: 2.0.2 + micromatch: 3.1.10 + minimist: 1.2.8 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color - rc@1.2.8: + /sane@5.0.1: + resolution: {integrity: sha512-9/0CYoRz0MKKf04OMCO3Qk3RQl1PAwWAhPSQSym4ULiLpTZnrY1JoZU0IEikHu8kdk2HvKT/VwQMq/xFZ8kh1Q==} + engines: {node: 10.* || >= 12.*} + hasBin: true dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 + '@cnakazawa/watch': 1.0.4 + anymatch: 3.1.3 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 4.1.0 + fb-watchman: 2.0.2 + micromatch: 4.0.8 minimist: 1.2.8 - strip-json-comments: 2.0.1 + walker: 1.0.8 - read-ini-file@4.0.0: + /saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} dependencies: - ini: 3.0.1 - strip-bom: 4.0.0 + xmlchars: 2.2.0 + dev: true - read-yaml-file@2.1.0: + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} dependencies: - js-yaml: 4.1.0 - strip-bom: 4.0.0 + xmlchars: 2.2.0 - readable-stream@1.0.34: + /schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) - readable-stream@3.6.2: + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) - readdirp@3.6.0: + /schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} dependencies: - picomatch: 2.3.1 + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1 + ajv-keywords: 5.1.0(ajv@8.17.1) - realpath-missing@1.1.0: {} + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true - recast@0.18.10: - dependencies: - ast-types: 0.13.3 - esprima: 4.0.1 - private: 0.1.8 - source-map: 0.6.1 + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true - recast@0.19.1: + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true dependencies: - ast-types: 0.13.3 - esprima: 4.0.1 - private: 0.1.8 - source-map: 0.6.1 + lru-cache: 6.0.0 + dev: false - redeyed@1.0.1: - dependencies: - esprima: 3.0.0 + /semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true - regenerate-unicode-properties@10.1.0: + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} dependencies: - regenerate: 1.4.2 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true - regenerate@1.4.2: {} + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color - regenerator-runtime@0.11.1: {} + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 - regenerator-runtime@0.13.11: {} + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color - regenerator-runtime@0.14.1: {} + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - regenerator-transform@0.10.1: + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} dependencies: - babel-runtime: 6.26.0 - babel-types: 6.26.0 - private: 0.1.8 + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 - regenerator-transform@0.15.2: + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} dependencies: - '@babel/runtime': 7.24.5 + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 - regex-not@1.0.2: + /set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} dependencies: - extend-shallow: 3.0.2 - safe-regex: 1.1.0 + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 - regexp.prototype.flags@1.4.3: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 + /setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - regexpp@3.2.0: {} + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - regexpu-core@2.0.0: + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} dependencies: - regenerate: 1.4.2 - regjsgen: 0.2.0 - regjsparser: 0.1.5 + shebang-regex: 1.0.0 - regexpu-core@5.3.2: + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 + shebang-regex: 3.0.0 - registry-auth-token@4.2.2: - dependencies: - rc: 1.2.8 + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} - registry-url@5.1.0: - dependencies: - rc: 1.2.8 + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} - regjsgen@0.2.0: {} + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: true - regjsparser@0.1.5: - dependencies: - jsesc: 0.5.0 + /shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} - regjsparser@0.9.1: + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} dependencies: - jsesc: 0.5.0 + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 - remote-git-tags@3.0.0: {} + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - remove-trailing-separator@1.1.0: {} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} - remove-types@1.0.0: + /silent-error@1.1.1: + resolution: {integrity: sha512-n4iEKyNcg4v6/jpb3c0/iyH2G1nzUNl7Gpqtn/mHIJK9S/q/7MCfoO4rwVOoO59qPFIc0hVHvMbiOJ0NdtxKKw==} dependencies: - '@babel/core': 7.24.5 - '@babel/plugin-syntax-decorators': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) - prettier: 2.8.7 + debug: 2.6.9 transitivePeerDependencies: - supports-color - repeat-element@1.1.4: {} + /simple-dom@1.4.0: + resolution: {integrity: sha512-TnBPkmOyjdaOqyBMb4ick+n8c0Xv9Iwg1PykFV7hz9Se3UCiacTbRb+25cPmvozFNJLBUNvUzX/KsPfXF14ivA==} + dependencies: + '@simple-dom/document': 1.4.0 + '@simple-dom/interface': 1.4.0 + '@simple-dom/parser': 1.4.0 + '@simple-dom/serializer': 1.4.0 + '@simple-dom/void-map': 1.4.0 + dev: true - repeat-string@1.6.1: {} + /simple-html-tokenizer@0.5.11: + resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} - request@2.40.0: - dependencies: - forever-agent: 0.5.2 - json-stringify-safe: 5.0.1 - mime-types: 1.0.2 - node-uuid: 1.4.8 - qs: 1.0.2 - optionalDependencies: - aws-sign2: 0.5.0 - form-data: 0.1.4 - hawk: 1.1.1 - http-signature: 0.10.1 - oauth-sign: 0.3.0 - stringstream: 0.0.6 - tough-cookie: 4.1.2 - tunnel-agent: 0.4.3 + /slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} - require-directory@2.1.1: {} + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: true - require-from-string@2.0.2: {} + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: true - requireindex@1.2.0: {} + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + dev: true - requires-port@1.0.0: {} + /snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 - reselect@3.0.1: {} + /snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 - reselect@4.1.7: {} + /snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color - resolve-dir@1.0.1: + /sntp@0.2.4: + resolution: {integrity: sha512-bDLrKa/ywz65gCl+LmOiIhteP1bhEsAAzhfMedPoiHP3dyYnAevlaJshdqb9Yu0sRifyP/fRqSt8t+5qGIWlGQ==} + engines: {node: '>=0.8.0'} + deprecated: This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. + requiresBuild: true dependencies: - expand-tilde: 2.0.2 - global-modules: 1.0.0 + hoek: 0.9.1 + dev: true + optional: true - resolve-from@4.0.0: {} + /socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + dependencies: + debug: 4.3.7(supports-color@8.1.1) + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate - resolve-package-path@1.2.7: + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} dependencies: - path-root: 0.1.1 - resolve: 1.22.1 + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - resolve-package-path@2.0.0: + /socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} dependencies: - path-root: 0.1.1 - resolve: 1.22.1 + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7(supports-color@8.1.1) + engine.io: 6.6.2 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate - resolve-package-path@3.1.0: + /socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} dependencies: - path-root: 0.1.1 - resolve: 1.22.1 + agent-base: 6.0.2 + debug: 4.3.7(supports-color@8.1.1) + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + dev: true - resolve-package-path@4.0.3: + /socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} dependencies: - path-root: 0.1.1 + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: true - resolve-path@1.4.0: + /sort-keys@4.2.0: + resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==} + engines: {node: '>=8'} dependencies: - http-errors: 1.6.3 - path-is-absolute: 1.0.1 + is-plain-obj: 2.1.0 - resolve-url@0.2.1: {} + /sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - resolve@1.22.1: + /sort-package-json@1.57.0: + resolution: {integrity: sha512-FYsjYn2dHTRb41wqnv+uEqCUvBpK3jZcTp9rbz2qDTmel7Pmdtf+i2rLaaPMRZeSVM60V3Se31GyWFpmKs4Q5Q==} + hasBin: true dependencies: - is-core-module: 2.11.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + detect-indent: 6.1.0 + detect-newline: 3.1.0 + git-hooks-list: 1.0.3 + globby: 10.0.0 + is-plain-obj: 2.1.0 + sort-object-keys: 1.1.3 + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} - responselike@1.0.2: + /source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated dependencies: - lowercase-keys: 1.0.1 + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 - restore-cursor@2.0.0: + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: - onetime: 2.0.1 - signal-exit: 3.0.7 + buffer-from: 1.1.2 + source-map: 0.6.1 - restore-cursor@3.1.0: + /source-map-url@0.3.0: + resolution: {integrity: sha512-QU4fa0D6aSOmrT+7OHpUXw+jS84T0MLaQNtFs8xzLNe6Arj44Magd7WEbyVW5LNYoAPVV35aKs4azxIfVJrToQ==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + /source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + /source-map@0.4.4: + resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} + engines: {node: '>=0.8.0'} dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 + amdefine: 1.0.1 - ret@0.1.15: {} + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} - retry@0.12.0: {} + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} - reusify@1.0.4: {} + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead - right-pad@1.0.1: {} + /spawn-args@0.2.0: + resolution: {integrity: sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==} - rimraf@2.6.3: + /split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} dependencies: - glob: 7.2.3 + extend-shallow: 3.0.2 - rimraf@2.7.1: + /split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} dependencies: - glob: 7.2.3 + readable-stream: 3.6.2 - rimraf@3.0.2: - dependencies: - glob: 7.2.3 + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - rimraf@4.4.1: - dependencies: - glob: 9.3.4 + /sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - rollup-plugin-copy-assets@2.0.3(rollup@3.20.2): - dependencies: - fs-extra: 7.0.1 - rollup: 3.20.2 + /sri-toolbox@0.2.0: + resolution: {integrity: sha512-DQIMWCAr/M7phwo+d3bEfXwSBEwuaJL+SJx9cuqt1Ty7K96ZFoHpYnSbhrQZEr0+0/GtmpKECP8X/R4RyeTAfw==} + engines: {node: '>= 0.10.4'} + dev: true - rollup-plugin-copy-assets@2.0.3(rollup@4.17.2): + /ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} dependencies: - fs-extra: 7.0.1 - rollup: 4.17.2 + minipass: 3.3.6 + dev: true - rollup-plugin-delete@2.0.0: + /stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} dependencies: - del: 5.1.0 + as-table: 1.0.55 + get-source: 2.0.12 - rollup-pluginutils@2.8.2: + /stagehand@1.0.1: + resolution: {integrity: sha512-GqXBq2SPWv9hTXDFKS8WrKK1aISB0aKGHZzH+uD4ShAgs+Fz20ZfoerLOm8U+f62iRWLrw6nimOY/uYuTcVhvg==} + engines: {node: 6.* || 8.* || >= 10.*} dependencies: - estree-walker: 0.6.1 - - rollup@2.79.1: - optionalDependencies: - fsevents: 2.3.2 - - rollup@3.20.2: - optionalDependencies: - fsevents: 2.3.2 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color - rollup@4.17.2: - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.17.2 - '@rollup/rollup-android-arm64': 4.17.2 - '@rollup/rollup-darwin-arm64': 4.17.2 - '@rollup/rollup-darwin-x64': 4.17.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 - '@rollup/rollup-linux-arm-musleabihf': 4.17.2 - '@rollup/rollup-linux-arm64-gnu': 4.17.2 - '@rollup/rollup-linux-arm64-musl': 4.17.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 - '@rollup/rollup-linux-riscv64-gnu': 4.17.2 - '@rollup/rollup-linux-s390x-gnu': 4.17.2 - '@rollup/rollup-linux-x64-gnu': 4.17.2 - '@rollup/rollup-linux-x64-musl': 4.17.2 - '@rollup/rollup-win32-arm64-msvc': 4.17.2 - '@rollup/rollup-win32-ia32-msvc': 4.17.2 - '@rollup/rollup-win32-x64-msvc': 4.17.2 - fsevents: 2.3.2 - - route-recognizer@0.3.4: {} - - router_js@8.0.5(route-recognizer@0.3.4)(rsvp@4.8.5): + /static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} dependencies: - '@glimmer/env': 0.1.7 - route-recognizer: 0.3.4 - rsvp: 4.8.5 + define-property: 0.2.5 + object-copy: 0.1.0 + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} - rsvp@3.2.1: {} + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} - rsvp@3.6.2: {} + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: false - rsvp@4.8.5: {} + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 - run-async@2.4.1: {} + /string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} - run-parallel@1.2.0: + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} dependencies: - queue-microtask: 1.2.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 - rxjs@6.6.7: + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} dependencies: - tslib: 1.14.1 + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 - rxjs@7.8.0: + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} dependencies: - tslib: 2.5.0 + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true - rxjs@7.8.1: + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} dependencies: - tslib: 2.5.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 - safe-buffer@5.1.2: {} + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 - safe-buffer@5.2.1: {} + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 - safe-execa@0.1.2: + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} dependencies: - '@zkochan/which': 2.0.3 - execa: 5.1.1 - path-name: 1.0.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 - safe-json-parse@1.0.1: {} + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - safe-regex-test@1.0.0: + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - is-regex: 1.1.4 + safe-buffer: 5.2.1 - safe-regex@1.1.0: + /stringify-object-es5@2.5.0: + resolution: {integrity: sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==} + engines: {node: '>=0.10.0'} dependencies: - ret: 0.1.15 - - safe-stable-stringify@2.4.3: {} + is-plain-obj: 1.1.0 + is-regexp: 1.0.0 + dev: true - safer-buffer@2.1.2: {} + /stringstream@0.0.6: + resolution: {integrity: sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==} + requiresBuild: true + dev: true + optional: true - sane@4.1.0: + /strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} dependencies: - '@cnakazawa/watch': 1.0.4 - anymatch: 2.0.0 - capture-exit: 2.0.0 - exec-sh: 0.3.6 - execa: 1.0.0 - fb-watchman: 2.0.2 - micromatch: 3.1.10 - minimist: 1.2.8 - walker: 1.0.8 - transitivePeerDependencies: - - supports-color + ansi-regex: 2.1.1 + dev: true - sane@5.0.1: + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} dependencies: - '@cnakazawa/watch': 1.0.4 - anymatch: 3.1.3 - capture-exit: 2.0.0 - exec-sh: 0.3.6 - execa: 4.1.0 - fb-watchman: 2.0.2 - micromatch: 4.0.5 - minimist: 1.2.8 - walker: 1.0.8 + ansi-regex: 3.0.1 - saxes@5.0.1: + /strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} dependencies: - xmlchars: 2.2.0 + ansi-regex: 4.1.1 - schema-utils@2.7.1: + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} dependencies: - '@types/json-schema': 7.0.11 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ansi-regex: 5.0.1 - schema-utils@3.1.1: + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} dependencies: - '@types/json-schema': 7.0.11 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ansi-regex: 6.1.0 + dev: true - schema-utils@4.0.0: - dependencies: - '@types/json-schema': 7.0.11 - ajv: 8.12.0 - ajv-formats: 2.1.1 - ajv-keywords: 5.1.0(ajv@8.12.0) + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: false - semver@5.7.1: {} + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} - semver@6.3.0: {} + /strip-comments-strings@1.2.0: + resolution: {integrity: sha512-zwF4bmnyEjZwRhaak9jUWNxc0DoeKBJ7lwSN/LEc8dQXZcUFG6auaaTQJokQWXopLdM3iTx01nQT8E4aL29DAQ==} - semver@6.3.1: {} + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} - semver@7.3.8: - dependencies: - lru-cache: 6.0.0 + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} - semver@7.6.0: - dependencies: - lru-cache: 6.0.0 + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true - send@0.18.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color + /strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + dev: true - serialize-javascript@6.0.0: - dependencies: - randombytes: 2.1.0 + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true - serialize-javascript@6.0.1: - dependencies: - randombytes: 2.1.0 + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} - serve-static@1.15.0: - dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.18.0 - transitivePeerDependencies: - - supports-color + /stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + + /style-loader@2.0.0(webpack@5.94.0): + resolution: {integrity: sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: 5.94.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.94.0 - set-blocking@2.0.0: {} + /styled_string@0.0.1: + resolution: {integrity: sha512-DU2KZiB6VbPkO2tGSqQ9n96ZstUPjW7X4sGO6V2m1myIQluX0p1Ol8BrA/l6/EesqhMqXOIXs3cJNOy1UuU2BA==} - set-value@2.0.1: + /sum-up@1.0.3: + resolution: {integrity: sha512-zw5P8gnhiqokJUWRdR6F4kIIIke0+ubQSGyYUY506GCbJWtV7F6Xuy0j6S125eSX2oF+a8KdivsZ8PlVEH0Mcw==} dependencies: - extend-shallow: 2.0.1 - is-extendable: 0.1.1 - is-plain-object: 2.0.4 - split-string: 3.1.0 - - setprototypeof@1.1.0: {} + chalk: 1.1.3 + dev: true - setprototypeof@1.2.0: {} + /supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + dev: true - shebang-command@1.2.0: + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} dependencies: - shebang-regex: 1.0.0 + has-flag: 3.0.0 - shebang-command@2.0.0: + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} dependencies: - shebang-regex: 3.0.0 - - shebang-regex@1.0.0: {} + has-flag: 4.0.0 - shebang-regex@3.0.0: {} + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 - shellwords@0.1.1: {} + /supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} - side-channel@1.0.4: - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - object-inspect: 1.12.3 + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} - signal-exit@3.0.7: {} + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - signal-exit@4.1.0: {} + /symlink-or-copy@1.3.1: + resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} - silent-error@1.1.1: + /sync-disk-cache@1.3.4: + resolution: {integrity: sha512-GlkGeM81GPPEKz/lH7QUTbvqLq7K/IUTuaKDSMulP9XQ42glqNJIN/RKgSOw4y8vxL1gOVvj+W7ruEO4s36eCw==} dependencies: debug: 2.6.9 + heimdalljs: 0.2.6 + mkdirp: 0.5.6 + rimraf: 2.7.1 + username-sync: 1.0.3 transitivePeerDependencies: - supports-color - simple-dom@1.4.0: + /sync-disk-cache@2.1.0: + resolution: {integrity: sha512-vngT2JmkSapgq0z7uIoYtB9kWOOzMihAAYq/D3Pjm/ODOGMgS4r++B+OZ09U4hWR6EaOdy9eqQ7/8ygbH3wehA==} + engines: {node: 8.* || >= 10.*} dependencies: - '@simple-dom/document': 1.4.0 - '@simple-dom/interface': 1.4.0 - '@simple-dom/parser': 1.4.0 - '@simple-dom/serializer': 1.4.0 - '@simple-dom/void-map': 1.4.0 - - simple-html-tokenizer@0.5.11: {} + debug: 4.3.7(supports-color@8.1.1) + heimdalljs: 0.2.6 + mkdirp: 0.5.6 + rimraf: 3.0.2 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color - slash@2.0.0: {} + /synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + dev: true - slash@3.0.0: {} + /tap-parser@7.0.0: + resolution: {integrity: sha512-05G8/LrzqOOFvZhhAk32wsGiPZ1lfUrl+iV7+OkKgfofZxiceZWMHkKmow71YsyVQ8IvGBP2EjcIjE5gL4l5lA==} + hasBin: true + dependencies: + events-to-array: 1.1.2 + js-yaml: 3.14.1 + minipass: 2.9.0 - smart-buffer@4.2.0: {} + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} - snake-case@3.0.4: + /tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} dependencies: - dot-case: 3.0.4 - tslib: 2.5.0 + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: true - snapdragon-node@2.1.1: + /temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} dependencies: - define-property: 1.0.0 - isobject: 3.0.1 - snapdragon-util: 3.0.1 + mkdirp: 0.5.6 + rimraf: 2.6.3 - snapdragon-util@3.0.1: + /terser-webpack-plugin@5.3.10(webpack@5.94.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: 5.94.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true dependencies: - kind-of: 3.2.2 + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.94.0 - snapdragon@0.8.2: + /terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + engines: {node: '>=10'} + hasBin: true dependencies: - base: 0.11.2 - debug: 2.6.9 - define-property: 0.2.5 - extend-shallow: 2.0.1 - map-cache: 0.2.2 - source-map: 0.5.7 - source-map-resolve: 0.5.3 - use: 3.1.1 - transitivePeerDependencies: - - supports-color + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 - sntp@0.2.4: + /testdouble@3.20.2: + resolution: {integrity: sha512-790e9vJKdfddWNOaxW1/V9FcMk48cPEl3eJSj2i8Hh1fX89qArEJ6cp3DBnaECpGXc3xKJVWbc1jeNlWYWgiMg==} + engines: {node: '>= 16'} dependencies: - hoek: 0.9.1 - optional: true + lodash: 4.17.21 + quibble: 0.9.2 + stringify-object-es5: 2.5.0 + theredoc: 1.0.0 + dev: true - socket.io-adapter@2.5.2: + /testem@3.11.0(patch_hash=yfkum5c5nfihh3ce3f64tnp5rq)(lodash@4.17.21): + resolution: {integrity: sha512-q0U126/nnRH54ZDrr6j1Ai5zK6vOm2rdY/5VJrbqcEPQgOWoLB6zrymWUs7BqN2/yRsdorocl9E9ZEwm7LLIZQ==} + engines: {node: '>= 7.*'} + hasBin: true dependencies: - ws: 8.11.0 + '@xmldom/xmldom': 0.8.10 + backbone: 1.6.0 + bluebird: 3.7.2 + charm: 1.0.2 + commander: 2.20.3 + compression: 1.7.5 + consolidate: 0.16.0(lodash@4.17.21)(mustache@4.2.0) + execa: 1.0.0 + express: 4.21.1 + fireworm: 0.7.2 + glob: 7.2.3 + http-proxy: 1.18.1 + js-yaml: 3.14.1 + lodash.assignin: 4.2.0 + lodash.castarray: 4.4.0 + lodash.clonedeep: 4.5.0 + lodash.find: 4.6.0 + lodash.uniqby: 4.7.0 + mkdirp: 3.0.1 + mustache: 4.2.0 + node-notifier: 10.0.1 + npmlog: 6.0.2 + printf: 0.6.1 + rimraf: 3.0.2 + socket.io: 4.8.1 + spawn-args: 0.2.0 + styled_string: 0.0.1 + tap-parser: 7.0.0 + tmp: 0.0.33 transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore - utf-8-validate + - vash + - velocityjs + - walrus + - whiskers + patched: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + /textextensions@2.6.0: + resolution: {integrity: sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==} + engines: {node: '>=0.8'} + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true - socket.io-parser@4.2.2: + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: - '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color + any-promise: 1.3.0 + dev: true - socket.io@4.6.1: + /theredoc@1.0.0: + resolution: {integrity: sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==} + dev: true + + /thread-loader@3.0.4(webpack@5.94.0): + resolution: {integrity: sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: 5.94.0 dependencies: - accepts: 1.3.8 - base64id: 2.0.0 - debug: 4.3.4 - engine.io: 6.4.1 - socket.io-adapter: 2.5.2 - socket.io-parser: 4.2.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate + json-parse-better-errors: 1.0.2 + loader-runner: 4.3.0 + loader-utils: 2.0.4 + neo-async: 2.6.2 + schema-utils: 3.3.0 + webpack: 5.94.0 + dev: true - socks-proxy-agent@6.2.1: + /through2@3.0.2: + resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - socks: 2.7.1 - transitivePeerDependencies: - - supports-color + inherits: 2.0.4 + readable-stream: 3.6.2 - socks@2.7.1: + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} dependencies: - ip: 2.0.0 - smart-buffer: 4.2.0 + readable-stream: 3.6.2 - sort-keys@4.2.0: + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + /tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} dependencies: - is-plain-obj: 2.1.0 + globalyzer: 0.1.0 + globrex: 0.1.2 - sort-object-keys@1.1.3: {} + /tiny-lr@2.0.0: + resolution: {integrity: sha512-f6nh0VMRvhGx4KCeK1lQ/jaL0Zdb5WdR+Jk8q9OSUQnaSDxAEGH1fgqLZ+cMl5EW3F2MGnCsalBO1IsnnogW1Q==} + dependencies: + body: 5.1.0 + debug: 3.2.7 + faye-websocket: 0.11.4 + livereload-js: 3.4.1 + object-assign: 4.1.1 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color - sort-package-json@1.57.0: + /tiny-readdir@2.7.3: + resolution: {integrity: sha512-ae1CPk7/MRhdaSIfjytuCoCjcykCNfSH36MsD2Qq8A27apaVUV0nthOcCEjiBTTloBObq2ffvm0BycUayMWh3A==} dependencies: - detect-indent: 6.1.0 - detect-newline: 3.1.0 - git-hooks-list: 1.0.3 - globby: 10.0.0 - is-plain-obj: 2.1.0 - sort-object-keys: 1.1.3 + promise-make-counter: 1.0.1 - source-map-js@1.0.2: {} + /tldts-core@6.1.60: + resolution: {integrity: sha512-XHjoxak8SFQnHnmYHb3PcnW5TZ+9ErLZemZei3azuIRhQLw4IExsVbL3VZJdHcLeNaXq6NqawgpDPpjBOg4B5g==} - source-map-resolve@0.5.3: + /tldts@6.1.60: + resolution: {integrity: sha512-TYVHm7G9NCnhgqOsFalbX6MG1Po5F4efF+tLfoeiOGQq48Oqgwcgz8upY2R1BHWa4aDrj28RYx0dkYJ63qCFMg==} + hasBin: true dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.2 - resolve-url: 0.2.1 - source-map-url: 0.4.1 - urix: 0.1.0 + tldts-core: 6.1.60 + + /tmp-sync@1.1.2: + resolution: {integrity: sha512-npRDYJiMaPWhcLf6q06v/vA3o/ZG4hfHDiBuj1N3Yeh3GTkFQb1YLFs6inDGMWIHjGidl4Oc1+oXHNKKj5vkDQ==} + engines: {node: '>=0.8.0'} + dependencies: + fs-sync: 1.0.6 + osenv: 0.1.5 + dev: true - source-map-support@0.5.21: + /tmp@0.0.28: + resolution: {integrity: sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==} + engines: {node: '>=0.4.0'} dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map-url@0.3.0: {} - - source-map-url@0.4.1: {} + os-tmpdir: 1.0.2 - source-map@0.1.43: + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} dependencies: - amdefine: 1.0.1 + os-tmpdir: 1.0.2 - source-map@0.4.4: + /tmp@0.1.0: + resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} + engines: {node: '>=6'} dependencies: - amdefine: 1.0.1 + rimraf: 2.7.1 - source-map@0.5.7: {} + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} - source-map@0.6.1: {} + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - source-map@0.7.4: {} + /to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 - sourcemap-codec@1.4.8: {} + /to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + dev: true - sourcemap-validator@1.1.1: + /to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} dependencies: - jsesc: 0.3.0 - lodash.foreach: 4.5.0 - lodash.template: 4.5.0 - source-map: 0.1.43 + is-number: 3.0.0 + repeat-string: 1.6.1 - spawn-args@0.2.0: {} + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + requiresBuild: true + dependencies: + is-number: 7.0.0 - split-string@3.1.0: + /to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} dependencies: + define-property: 2.0.2 extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 - split2@3.2.2: - dependencies: - readable-stream: 3.6.2 + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} - sprintf-js@1.0.3: {} + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.10.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true - sprintf-js@1.1.2: {} + /tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + dependencies: + tldts: 6.1.60 - sri-toolbox@0.2.0: {} + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true - ssri@8.0.1: + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} dependencies: - minipass: 3.3.6 + punycode: 2.3.1 + dev: true - stacktracey@2.1.8: + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} dependencies: - as-table: 1.0.55 - get-source: 2.0.12 + punycode: 2.3.1 - stagehand@1.0.1: + /tracked-built-ins@3.3.0(@babel/core@7.26.0): + resolution: {integrity: sha512-ewKFrW/AQs05oLPM5isOUb/1aOwBRfHfmF408CCzTk21FLAhKrKVOP5Q5ebX+zCT4kvg81PGBGwrBiEGND1nWA==} dependencies: - debug: 4.3.4 + '@embroider/addon-shim': 1.9.0 + ember-tracked-storage-polyfill: 1.0.0(@babel/core@7.26.0) transitivePeerDependencies: + - '@babel/core' - supports-color + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true - static-extend@0.1.2: + /tree-sync@1.4.0: + resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} dependencies: - define-property: 0.2.5 - object-copy: 0.1.0 + debug: 2.6.9 + fs-tree-diff: 0.5.9 + mkdirp: 0.5.6 + quick-temp: 0.1.8 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color - statuses@1.5.0: {} + /tree-sync@2.1.0: + resolution: {integrity: sha512-OLWW+Nd99NOM53aZ8ilT/YpEiOo6mXD3F4/wLbARqybSZ3Jb8IxHK5UGVbZaae0wtXAyQshVV+SeqVBik+Fbmw==} + engines: {node: '>=8'} + dependencies: + debug: 4.3.7(supports-color@8.1.1) + fs-tree-diff: 2.0.1 + mkdirp: 0.5.6 + quick-temp: 0.1.8 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color - statuses@2.0.1: {} + /ts-api-utils@1.4.0(typescript@5.6.3): + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '*' + dependencies: + typescript: 5.6.3 - string-length@4.0.2: + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: false - string-template@0.2.1: {} + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - string-width@2.1.1: - dependencies: - is-fullwidth-code-point: 2.0.0 - strip-ansi: 4.0.0 + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 + /tunnel-agent@0.4.3: + resolution: {integrity: sha512-e0IoVDWx8SDHc/hwFTqJDQ7CCDTEeGhmcT9jkWJjoGQSpgBz20nAMr80E3Tpk7PatJ1b37DQDgJR3CNSzcMOZQ==} + requiresBuild: true + dev: true + optional: true - string.prototype.matchall@4.0.8: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 - get-intrinsic: 1.2.0 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - regexp.prototype.flags: 1.4.3 - side-channel: 1.0.4 + /turbo-darwin-64@1.13.4: + resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true - string.prototype.trim@1.2.7: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 + /turbo-darwin-arm64@1.13.4: + resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true - string.prototype.trimend@1.0.6: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 + /turbo-linux-64@1.13.4: + resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true - string.prototype.trimstart@1.0.6: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.2 + /turbo-linux-arm64@1.13.4: + resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true - string_decoder@0.10.31: {} + /turbo-windows-64@1.13.4: + resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /turbo-windows-arm64@1.13.4: + resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true - string_decoder@1.3.0: + /turbo@1.13.4: + resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==} + hasBin: true + optionalDependencies: + turbo-darwin-64: 1.13.4 + turbo-darwin-arm64: 1.13.4 + turbo-linux-64: 1.13.4 + turbo-linux-arm64: 1.13.4 + turbo-windows-64: 1.13.4 + turbo-windows-arm64: 1.13.4 + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} dependencies: - safe-buffer: 5.2.1 + prelude-ls: 1.2.1 + + /type-detect@0.1.1: + resolution: {integrity: sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==} + dev: true + + /type-detect@1.0.0: + resolution: {integrity: sha512-f9Uv6ezcpvCQjJU0Zqbg+65qdcszv3qUQsZfjdRbWiZ7AMenrX1u0lNk9EoWWX6e1F+NULyg27mtdeZ5WhpljA==} + dev: true + + /type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.11.0: + resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} + engines: {node: '>=8'} + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} - stringify-object-es5@2.5.0: + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} dependencies: - is-plain-obj: 1.1.0 - is-regexp: 1.0.0 + media-typer: 0.3.0 + mime-types: 2.1.35 - stringstream@0.0.6: - optional: true + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 - strip-ansi@3.0.1: + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} dependencies: - ansi-regex: 2.1.1 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 - strip-ansi@4.0.0: + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} dependencies: - ansi-regex: 3.0.1 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 - strip-ansi@5.2.0: + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} dependencies: - ansi-regex: 4.1.1 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 - strip-ansi@6.0.1: + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} dependencies: - ansi-regex: 5.0.1 + is-typedarray: 1.0.0 - strip-bom@3.0.0: {} + /typescript-eslint@8.14.0(eslint@9.14.0)(typescript@5.6.3): + resolution: {integrity: sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0)(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.14.0)(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color - strip-bom@4.0.0: {} + /typescript-memoize@1.1.1: + resolution: {integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==} - strip-comments-strings@1.2.0: {} + /typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: false - strip-eof@1.0.0: {} + /typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true - strip-final-newline@2.0.0: {} + /typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: true - strip-json-comments@2.0.1: {} + /uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - strip-json-comments@3.1.1: {} + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + optional: true - stubborn-fs@1.2.5: {} + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 - style-loader@2.0.0(webpack@5.77.0): + /underscore.string@3.3.6: + resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} dependencies: - loader-utils: 2.0.4 - schema-utils: 3.1.1 - webpack: 5.77.0 + sprintf-js: 1.1.3 + util-deprecate: 1.0.2 - styled_string@0.0.1: {} + /underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - sum-up@1.0.3: - dependencies: - chalk: 1.1.3 + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true - supports-color@2.0.0: {} + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 + /unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} - supports-color@7.2.0: + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} dependencies: - has-flag: 4.0.0 + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 + /unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} - supports-color@9.4.0: {} + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} - supports-preserve-symlinks-flag@1.0.0: {} + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: true - symbol-tree@3.2.4: {} + /unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + dev: true - symlink-or-copy@1.3.1: {} + /union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 - sync-disk-cache@1.3.4: + /unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} dependencies: - debug: 2.6.9 - heimdalljs: 0.2.6 - mkdirp: 0.5.6 - rimraf: 2.7.1 - username-sync: 1.0.3 - transitivePeerDependencies: - - supports-color + unique-slug: 2.0.2 + dev: true - sync-disk-cache@2.1.0: + /unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} dependencies: - debug: 4.3.4 - heimdalljs: 0.2.6 - mkdirp: 0.5.6 - rimraf: 3.0.2 - username-sync: 1.0.3 - transitivePeerDependencies: - - supports-color + imurmurhash: 0.1.4 + dev: true - tap-parser@7.0.0: + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} dependencies: - events-to-array: 1.1.2 - js-yaml: 3.14.1 - minipass: 2.9.0 + crypto-random-string: 2.0.0 + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} - tapable@2.2.1: {} + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true - tar@6.1.13: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 4.2.5 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} - temp@0.9.4: + /unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} dependencies: - mkdirp: 0.5.6 - rimraf: 2.6.3 + has-value: 0.3.1 + isobject: 3.0.1 - terser-webpack-plugin@5.3.7(webpack@5.77.0): + /upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db@1.1.1(browserslist@4.24.2): + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' dependencies: - '@jridgewell/trace-mapping': 0.3.17 - jest-worker: 27.5.1 - schema-utils: 3.1.1 - serialize-javascript: 6.0.1 - terser: 5.16.8 - webpack: 5.77.0 + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 - terser@5.16.8: + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - '@jridgewell/source-map': 0.3.2 - acorn: 8.8.2 - commander: 2.20.3 - source-map-support: 0.5.21 + punycode: 2.3.1 + + /urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated - testdouble@3.17.1: + /url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} dependencies: - lodash: 4.17.21 - quibble: 0.6.17 - stringify-object-es5: 2.5.0 - theredoc: 1.0.0 + prepend-http: 2.0.0 + dev: true - testem@3.10.1: + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} dependencies: - '@xmldom/xmldom': 0.8.6 - backbone: 1.4.1 - bluebird: 3.7.2 - charm: 1.0.2 - commander: 2.20.3 - compression: 1.7.4 - consolidate: 0.16.0(mustache@4.2.0) - execa: 1.0.0 - express: 4.18.2 - fireworm: 0.7.2 - glob: 7.2.3 - http-proxy: 1.18.1 - js-yaml: 3.14.1 - lodash.assignin: 4.2.0 - lodash.castarray: 4.4.0 - lodash.clonedeep: 4.5.0 - lodash.find: 4.6.0 - lodash.uniqby: 4.7.0 - mkdirp: 1.0.4 - mustache: 4.2.0 - node-notifier: 10.0.1 - npmlog: 6.0.2 - printf: 0.6.1 - rimraf: 3.0.2 - socket.io: 4.6.1 - spawn-args: 0.2.0 - styled_string: 0.0.1 - tap-parser: 7.0.0 - tmp: 0.0.33 - transitivePeerDependencies: - - arc-templates - - atpl - - babel-core - - bracket-template - - bufferutil - - coffee-script - - debug - - dot - - dust - - dustjs-helpers - - dustjs-linkedin - - eco - - ect - - ejs - - haml-coffee - - hamlet - - hamljs - - handlebars - - hogan.js - - htmling - - jade - - jazz - - jqtpl - - just - - liquid-node - - liquor - - lodash - - marko - - mote - - nunjucks - - plates - - pug - - qejs - - ractive - - razor-tmpl - - react - - react-dom - - slm - - squirrelly - - supports-color - - swig - - swig-templates - - teacup - - templayed - - then-jade - - then-pug - - tinyliquid - - toffee - - twig - - twing - - underscore - - utf-8-validate - - vash - - velocityjs - - walrus - - whiskers + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true - testem@3.10.1(debug@4.3.4): + /url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} dependencies: - '@xmldom/xmldom': 0.8.6 - backbone: 1.4.1 - bluebird: 3.7.2 - charm: 1.0.2 - commander: 2.20.3 - compression: 1.7.4 - consolidate: 0.16.0(mustache@4.2.0) - execa: 1.0.0 - express: 4.18.2 - fireworm: 0.7.2 - glob: 7.2.3 - http-proxy: 1.18.1(debug@4.3.4) - js-yaml: 3.14.1 - lodash.assignin: 4.2.0 - lodash.castarray: 4.4.0 - lodash.clonedeep: 4.5.0 - lodash.find: 4.6.0 - lodash.uniqby: 4.7.0 - mkdirp: 1.0.4 - mustache: 4.2.0 - node-notifier: 10.0.1 - npmlog: 6.0.2 - printf: 0.6.1 - rimraf: 3.0.2 - socket.io: 4.6.1 - spawn-args: 0.2.0 - styled_string: 0.0.1 - tap-parser: 7.0.0 - tmp: 0.0.33 - transitivePeerDependencies: - - arc-templates - - atpl - - babel-core - - bracket-template - - bufferutil - - coffee-script - - debug - - dot - - dust - - dustjs-helpers - - dustjs-linkedin - - eco - - ect - - ejs - - haml-coffee - - hamlet - - hamljs - - handlebars - - hogan.js - - htmling - - jade - - jazz - - jqtpl - - just - - liquid-node - - liquor - - lodash - - marko - - mote - - nunjucks - - plates - - pug - - qejs - - ractive - - razor-tmpl - - react - - react-dom - - slm - - squirrelly - - supports-color - - swig - - swig-templates - - teacup - - templayed - - then-jade - - then-pug - - tinyliquid - - toffee - - twig - - twing - - underscore - - utf-8-validate - - vash - - velocityjs - - walrus - - whiskers + punycode: 1.4.1 + qs: 6.13.0 + dev: true - text-table@0.2.0: {} + /use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} - textextensions@2.6.0: {} + /username-sync@1.0.3: + resolution: {integrity: sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==} - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} - theredoc@1.0.0: {} + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true - thread-loader@3.0.4(webpack@5.77.0): - dependencies: - json-parse-better-errors: 1.0.2 - loader-runner: 4.3.0 - loader-utils: 2.0.4 - neo-async: 2.6.2 - schema-utils: 3.1.1 - webpack: 5.77.0 + /v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + dev: true - through2@3.0.2: - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.2 + /validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - through2@4.0.2: + /validate-peer-dependencies@1.2.0: + resolution: {integrity: sha512-nd2HUpKc6RWblPZQ2GDuI65sxJ2n/UqZwSBVtj64xlWjMx0m7ZB2m9b2JS3v1f+n9VWH/dd1CMhkHfP6pIdckA==} dependencies: - readable-stream: 3.6.2 + resolve-package-path: 3.1.0 + semver: 7.6.3 + dev: true - through@2.3.8: {} + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + dev: false - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} - tiny-lr@2.0.0: + /vite-plugin-dts@3.9.1(rollup@4.25.0)(typescript@5.6.3)(vite@5.4.11): + resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true dependencies: - body: 5.1.0 - debug: 3.2.7 - faye-websocket: 0.11.4 - livereload-js: 3.4.1 - object-assign: 4.1.1 - qs: 6.11.1 - transitivePeerDependencies: + '@microsoft/api-extractor': 7.43.0 + '@rollup/pluginutils': 5.1.3(rollup@4.25.0) + '@vue/language-core': 1.8.27(typescript@5.6.3) + debug: 4.3.7(supports-color@8.1.1) + kolorist: 1.8.0 + magic-string: 0.30.12 + typescript: 5.6.3 + vite: 5.4.11(@types/node@20.17.6) + vue-tsc: 1.8.27(typescript@5.6.3) + transitivePeerDependencies: + - '@types/node' + - rollup - supports-color + dev: false - tiny-readdir@2.7.2: + /vite@5.4.11(@types/node@20.17.6): + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true dependencies: - promise-make-naked: 2.1.2 + '@types/node': 20.17.6 + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.25.0 + optionalDependencies: + fsevents: 2.3.3 - tmp-sync@1.1.2: - dependencies: - fs-sync: 1.0.6 - osenv: 0.1.5 + /vscode-jsonrpc@8.1.0: + resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==} + engines: {node: '>=14.0.0'} + dev: true - tmp@0.0.28: + /vscode-languageserver-protocol@3.17.3: + resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==} dependencies: - os-tmpdir: 1.0.2 + vscode-jsonrpc: 8.1.0 + vscode-languageserver-types: 3.17.3 + dev: true - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 + /vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + dev: true + + /vscode-languageserver-types@3.17.3: + resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==} + dev: true - tmp@0.1.0: + /vscode-languageserver@8.1.0: + resolution: {integrity: sha512-eUt8f1z2N2IEUDBsKaNapkz7jl5QpskN2Y0G01T/ItMxBxw1fJwvtySGB9QMecatne8jFIWJGWI61dWjyTLQsw==} + hasBin: true dependencies: - rimraf: 2.7.1 + vscode-languageserver-protocol: 3.17.3 + dev: true - tmpl@1.0.5: {} + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: true - to-fast-properties@1.0.3: {} + /vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + dev: false - to-fast-properties@2.0.0: {} + /vue-tsc@1.8.27(typescript@5.6.3): + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + hasBin: true + peerDependencies: + typescript: '*' + dependencies: + '@volar/typescript': 1.11.1 + '@vue/language-core': 1.8.27(typescript@5.6.3) + semver: 7.6.3 + typescript: 5.6.3 + dev: false - to-object-path@0.3.0: + /w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. dependencies: - kind-of: 3.2.2 - - to-readable-stream@1.0.0: {} + browser-process-hrtime: 1.0.0 + dev: true - to-regex-range@2.1.1: + /w3c-xmlserializer@3.0.0: + resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} + engines: {node: '>=12'} dependencies: - is-number: 3.0.0 - repeat-string: 1.6.1 + xml-name-validator: 4.0.0 + dev: true - to-regex-range@5.0.1: + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} dependencies: - is-number: 7.0.0 + xml-name-validator: 5.0.0 - to-regex@3.0.2: + /walk-sync@0.2.7: + resolution: {integrity: sha512-OH8GdRMowEFr0XSHQeX5fGweO6zSVHo7bG/0yJQx6LAj9Oukz0C8heI3/FYectT66gY0IPGe89kOvU410/UNpg==} dependencies: - define-property: 2.0.2 - extend-shallow: 3.0.2 - regex-not: 1.0.2 - safe-regex: 1.1.0 + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 + dev: true - toidentifier@1.0.1: {} + /walk-sync@0.3.4: + resolution: {integrity: sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig==} + dependencies: + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 - tough-cookie@4.1.2: + /walk-sync@1.1.4: + resolution: {integrity: sha512-nowc9thB/Jg0KW4TgxoRjLLYRPvl3DB/98S89r4ZcJqq2B0alNcKDh6pzLkBSkPMzRSMsJghJHQi79qw0YWEkA==} dependencies: - psl: 1.9.0 - punycode: 2.3.0 - universalify: 0.2.0 - url-parse: 1.5.10 + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 - tr46@0.0.3: {} + /walk-sync@2.2.0: + resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} + engines: {node: 8.* || >= 10.*} + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.2 - tr46@2.1.0: + /walk-sync@3.0.0: + resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} + engines: {node: 10.* || >= 12.*} dependencies: - punycode: 2.3.0 + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.2 - tr46@3.0.0: + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: - punycode: 2.3.0 + makeerror: 1.0.12 - tree-sync@1.4.0: + /watch-detector@0.1.0: + resolution: {integrity: sha512-vfzMMfpjQc88xjETwl2HuE6PjEuxCBeyC4bQmqrHrofdfYWi/4mEJklYbNgSzpqM9PxubsiPIrE5SZ1FDyiQ2w==} + engines: {node: '>= 4'} dependencies: - debug: 2.6.9 - fs-tree-diff: 0.5.9 - mkdirp: 0.5.6 + heimdalljs-logger: 0.1.10 quick-temp: 0.1.8 - walk-sync: 0.3.4 + rsvp: 4.8.5 + semver: 5.7.2 + silent-error: 1.1.1 transitivePeerDependencies: - supports-color + dev: true - tree-sync@2.1.0: + /watch-detector@1.0.2: + resolution: {integrity: sha512-MrJK9z7kD5Gl3jHBnnBVHvr1saVGAfmkyyrvuNzV/oe0Gr1nwZTy5VSA0Gw2j2Or0Mu8HcjUa44qlBvC2Ofnpg==} + engines: {node: '>= 8'} dependencies: - debug: 4.3.4 - fs-tree-diff: 2.0.1 - mkdirp: 0.5.6 - quick-temp: 0.1.8 - walk-sync: 0.3.4 + heimdalljs-logger: 0.1.10 + silent-error: 1.1.1 + tmp: 0.1.0 transitivePeerDependencies: - supports-color - tsconfig-paths@3.14.2: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - - tslib@1.14.1: {} - - tslib@2.5.0: {} - - tsutils@3.21.0(typescript@5.0.3): + /watcher@2.3.1: + resolution: {integrity: sha512-d3yl+ey35h05r5EFP0TafE2jsmQUJ9cc2aernRVyAkZiu0J3+3TbNugNcqdUJDoWOfL2p+bNsN427stsBC/HnA==} dependencies: - tslib: 1.14.1 - typescript: 5.0.3 - - tunnel-agent@0.4.3: - optional: true + dettle: 1.0.4 + stubborn-fs: 1.2.5 + tiny-readdir: 2.7.3 - type-check@0.3.2: + /watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} dependencies: - prelude-ls: 1.1.2 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 - type-check@0.4.0: + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: - prelude-ls: 1.2.1 - - type-detect@0.1.1: {} - - type-detect@1.0.0: {} - - type-detect@4.0.8: {} - - type-fest@0.11.0: {} + defaults: 1.0.4 - type-fest@0.20.2: {} + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true - type-fest@0.21.3: {} + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} - type-fest@0.6.0: {} + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} - type-is@1.6.18: + /webpack@5.94.0: + resolution: {integrity: sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true dependencies: - media-typer: 0.3.0 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.94.0) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js - typed-array-length@1.0.4: - dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - is-typed-array: 1.1.10 - - typedarray-to-buffer@3.1.5: + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} dependencies: - is-typedarray: 1.0.0 + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 - typescript-memoize@1.1.1: {} + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} - typescript@5.0.3: {} + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true - typescript@5.4.5: {} + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 - typical@4.0.0: {} + /whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + dev: true - uc.micro@1.0.6: {} + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true - uglify-js@3.17.4: - optional: true + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} - unbox-primitive@1.0.2: + /whatwg-url@10.0.0: + resolution: {integrity: sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==} + engines: {node: '>=12'} dependencies: - call-bind: 1.0.2 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true - underscore.string@3.3.6: + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} dependencies: - sprintf-js: 1.1.2 - util-deprecate: 1.0.2 - - underscore@1.13.6: {} - - unicode-canonical-property-names-ecmascript@2.0.0: {} + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true - unicode-match-property-ecmascript@2.0.0: + /whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - - unicode-match-property-value-ecmascript@2.1.0: {} - - unicode-property-aliases-ecmascript@2.1.0: {} + tr46: 5.0.0 + webidl-conversions: 7.0.0 - union-value@1.0.1: + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: - arr-union: 3.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - set-value: 2.0.1 + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true - unique-filename@1.1.1: + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: - unique-slug: 2.0.2 + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 - unique-slug@2.0.2: + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} dependencies: - imurmurhash: 0.1.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 - unique-string@2.0.0: + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true dependencies: - crypto-random-string: 2.0.0 - - universalify@0.1.2: {} - - universalify@0.2.0: {} - - universalify@2.0.0: {} - - unpipe@1.0.0: {} + isexe: 2.0.0 - unset-value@1.0.0: + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true dependencies: - has-value: 0.3.1 - isobject: 3.0.1 + isexe: 2.0.0 - untildify@2.1.0: + /which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true dependencies: - os-homedir: 1.0.2 + isexe: 2.0.0 - update-browserslist-db@1.0.10(browserslist@4.21.5): + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: - browserslist: 4.21.5 - escalade: 3.1.1 - picocolors: 1.0.0 + string-width: 4.2.3 - update-browserslist-db@1.0.13(browserslist@4.23.0): + /widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} dependencies: - browserslist: 4.23.0 - escalade: 3.1.1 - picocolors: 1.0.0 + string-width: 4.2.3 - uri-js@4.4.1: - dependencies: - punycode: 2.3.0 + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} - urix@0.1.0: {} + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - url-parse-lax@3.0.0: - dependencies: - prepend-http: 2.0.0 + /workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} - url-parse@1.5.10: + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 - url@0.11.0: + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - - use@3.1.1: {} - - username-sync@1.0.3: {} - - util-deprecate@1.0.2: {} + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 - utils-merge@1.0.1: {} + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true - uuid@8.3.2: {} + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - validate-npm-package-name@5.0.0: + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} dependencies: - builtins: 5.0.1 + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 - validate-peer-dependencies@1.2.0: + /write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - resolve-package-path: 3.1.0 - semver: 7.6.0 + imurmurhash: 0.1.4 + signal-exit: 4.1.0 - validate-peer-dependencies@2.2.0: + /write-yaml-file@5.0.0: + resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==} + engines: {node: '>=16.14'} dependencies: - resolve-package-path: 4.0.3 - semver: 7.6.0 + js-yaml: 4.1.0 + write-file-atomic: 5.0.1 - vary@1.1.2: {} + /ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true - w3c-hr-time@1.0.2: - dependencies: - browser-process-hrtime: 1.0.0 + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true - w3c-xmlserializer@2.0.0: - dependencies: - xml-name-validator: 3.0.0 + /xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} - w3c-xmlserializer@3.0.0: - dependencies: - xml-name-validator: 4.0.0 + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true - walk-sync@0.2.7: - dependencies: - ensure-posix-path: 1.1.1 - matcher-collection: 1.1.2 + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - walk-sync@0.3.4: - dependencies: - ensure-posix-path: 1.1.1 - matcher-collection: 1.1.2 + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true - walk-sync@1.1.4: - dependencies: - '@types/minimatch': 3.0.5 - ensure-posix-path: 1.1.1 - matcher-collection: 1.1.2 + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} - walk-sync@2.2.0: - dependencies: - '@types/minimatch': 3.0.5 - ensure-posix-path: 1.1.1 - matcher-collection: 2.0.1 - minimatch: 3.1.2 + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - walk-sync@3.0.0: - dependencies: - '@types/minimatch': 3.0.5 - ensure-posix-path: 1.1.1 - matcher-collection: 2.0.1 - minimatch: 3.1.2 + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - walker@1.0.8: + /yam@1.0.0: + resolution: {integrity: sha512-Hv9xxHtsJ9228wNhk03xnlDReUuWVvHwM4rIbjdAXYvHLs17xjuyF50N6XXFMN6N0omBaqgOok/MCK3At9fTAg==} + engines: {node: ^4.5 || 6.* || >= 7.*} dependencies: - makeerror: 1.0.12 + fs-extra: 4.0.3 + lodash.merge: 4.6.2 - watch-detector@0.1.0: - dependencies: - heimdalljs-logger: 0.1.10 - quick-temp: 0.1.8 - rsvp: 4.8.5 - semver: 5.7.1 - silent-error: 1.1.1 - transitivePeerDependencies: - - supports-color + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true - watch-detector@1.0.2: - dependencies: - heimdalljs-logger: 0.1.10 - silent-error: 1.1.1 - tmp: 0.1.0 - transitivePeerDependencies: - - supports-color + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} - watcher@2.3.1: + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} dependencies: - dettle: 1.0.2 - stubborn-fs: 1.2.5 - tiny-readdir: 2.7.2 + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true - watchpack@2.4.0: + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true - wcwidth@1.0.1: + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} dependencies: - defaults: 1.0.4 + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 - webidl-conversions@3.0.1: {} + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} - webidl-conversions@5.0.0: {} + /yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + dev: true - webidl-conversions@6.1.0: {} + /yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} - webidl-conversions@7.0.0: {} + /yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + dev: true - webpack-sources@3.2.3: {} + /yui@3.18.1: + resolution: {integrity: sha512-M4/mHnq5uGvpwKEpRBh3SclL70cpDEus9LNGnrK5ZBzp4HOoueY7EkXfgtRBd+9VOQHWlFukXL2udHE53N4Wqw==} + engines: {node: '>=0.8.0'} + dependencies: + request: 2.40.0 + dev: true - webpack@5.77.0: + /yuidocjs@0.10.2: + resolution: {integrity: sha512-g0ZrXsaCmQL9zsvkgD+RxWDsMNkHne5tK72iWYodro9JQlfKxePcV1dwbGhKMy/fl1XCIW3R3erZudohU+PcEw==} + engines: {node: '>=0.10.0'} + hasBin: true dependencies: - '@types/eslint-scope': 3.7.4 - '@types/estree': 0.0.51 - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/wasm-edit': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.8.2 - acorn-import-assertions: 1.8.0(acorn@8.8.2) - browserslist: 4.21.5 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.12.0 - es-module-lexer: 0.9.3 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 + express: 4.21.1 graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.1.1 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.7(webpack@5.77.0) - watchpack: 2.4.0 - webpack-sources: 3.2.3 + markdown-it: 4.4.0 + mdn-links: 0.1.0 + minimatch: 3.1.2 + rimraf: 2.7.1 + yui: 3.18.1 transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js + - supports-color + dev: true - websocket-driver@0.7.4: + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true dependencies: - http-parser-js: 0.5.8 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.12.0 + optionalDependencies: + commander: 9.5.0 + dev: false - websocket-extensions@0.1.4: {} + /zlib@1.0.5: + resolution: {integrity: sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==} + engines: {node: '>=0.2.0'} + dev: true - whatwg-encoding@1.0.5: + file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@3.1.1)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(ember-inflector@4.0.3)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {directory: packages/-ember-data, type: directory} + id: file:packages/-ember-data + name: ember-data + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/test-helpers': ^3.3.0 || ^4.0.4 + '@ember/test-waiters': ^3.1.0 + ember-source: '*' + qunit: 2.19.4 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@ember/test-waiters': + optional: true + qunit: + optional: true dependencies: - iconv-lite: 0.4.24 + '@ember-data/adapter': file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + transitivePeerDependencies: + - '@babel/core' + - '@ember/string' + - '@glint/template' + - ember-inflector + - supports-color + dev: true - whatwg-encoding@2.0.0: + file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@3.1.1)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-inflector@4.0.3)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {directory: packages/-ember-data, type: directory} + id: file:packages/-ember-data + name: ember-data + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/test-helpers': ^3.3.0 || ^4.0.4 + '@ember/test-waiters': ^3.1.0 + ember-source: '*' + qunit: 2.19.4 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@ember/test-waiters': + optional: true + qunit: + optional: true dependencies: - iconv-lite: 0.6.3 - - whatwg-fetch@3.6.2: {} + '@ember-data/adapter': file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + transitivePeerDependencies: + - '@babel/core' + - '@ember/string' + - '@glint/template' + - ember-inflector + - supports-color - whatwg-mimetype@2.3.0: {} + file:packages/-ember-data(@babel/core@7.26.0)(@ember/string@4.0.0)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-source@5.12.0)(qunit@2.19.4): + resolution: {directory: packages/-ember-data, type: directory} + id: file:packages/-ember-data + name: ember-data + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/test-helpers': ^3.3.0 || ^4.0.4 + '@ember/test-waiters': ^3.1.0 + ember-source: '*' + qunit: 2.19.4 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@ember/test-waiters': + optional: true + qunit: + optional: true + dependencies: + '@ember-data/adapter': file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@4.0.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/serializer': file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + transitivePeerDependencies: + - '@babel/core' + - '@ember/string' + - '@glint/template' + - ember-inflector + - supports-color + dev: true - whatwg-mimetype@3.0.0: {} + file:packages/-ember-data(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(@ember/test-waiters@3.1.0)(ember-source@5.12.0): + resolution: {directory: packages/-ember-data, type: directory} + id: file:packages/-ember-data + name: ember-data + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/test-helpers': ^3.3.0 || ^4.0.4 + '@ember/test-waiters': ^3.1.0 + ember-source: '*' + qunit: 2.19.4 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@ember/test-waiters': + optional: true + qunit: + optional: true + dependencies: + '@ember-data/adapter': file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/debug': file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/serializer': file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@ember/string' + - '@glint/template' + - ember-inflector + - supports-color - whatwg-url@10.0.0: + file:packages/active-record(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/active-record, type: directory} + id: file:packages/active-record + name: '@ember-data/active-record' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request-utils': workspace:* + '@ember-data/store': ^4.12.0 || ^5.0.0 + '@warp-drive/core-types': workspace:* dependencies: - tr46: 3.0.0 - webidl-conversions: 7.0.0 + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - whatwg-url@11.0.0: + file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/adapter, type: directory} + id: file:packages/adapter + name: '@ember-data/adapter' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - tr46: 3.0.0 - webidl-conversions: 7.0.0 + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - whatwg-url@5.0.0: + file:packages/adapter(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/adapter, type: directory} + id: file:packages/adapter + name: '@ember-data/adapter' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - whatwg-url@8.7.0: + file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0): + resolution: {directory: packages/build-config, type: directory} + id: file:packages/build-config + name: '@warp-drive/build-config' + engines: {node: '>= 18.20.4'} dependencies: - lodash: 4.17.21 - tr46: 2.1.0 - webidl-conversions: 6.1.0 + '@embroider/addon-shim': 1.9.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + babel-import-util: 2.1.1 + broccoli-funnel: 3.0.8 + semver: 7.6.3 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - which-boxed-primitive@1.0.2: + file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0): + resolution: {directory: packages/core-types, type: directory} + id: file:packages/core-types + name: '@warp-drive/core-types' + engines: {node: '>= 18.20.4'} dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - which-typed-array@1.1.9: + file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/debug, type: directory} + id: file:packages/debug + name: '@ember-data/debug' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/model': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - which@1.3.1: + file:packages/debug(@babel/core@7.26.0)(@ember-data/model@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/debug, type: directory} + id: file:packages/debug + name: '@ember-data/debug' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/model': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - isexe: 2.0.0 + '@ember-data/model': file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - which@2.0.2: + file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0): + resolution: {directory: packages/diagnostic, type: directory} + id: file:packages/diagnostic + name: '@warp-drive/diagnostic' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/test-helpers': 4.0.4 + ember-cli-test-loader: '>= 3.1.0' + ember-source: '*' + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + ember-cli-test-loader: + optional: true + ember-source: + optional: true dependencies: - isexe: 2.0.0 + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + chalk: 5.3.0 + debug: 4.3.7(supports-color@8.1.1) + ember-cli-htmlbars: 6.3.0 + ember-cli-test-loader: 3.1.0(@babel/core@7.26.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + tmp: 0.2.3 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - which@3.0.1: + file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/graph, type: directory} + id: file:packages/graph + name: '@ember-data/graph' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - isexe: 2.0.0 + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - wide-align@1.1.5: + file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/graph, type: directory} + id: file:packages/graph + name: '@ember-data/graph' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - string-width: 4.2.3 + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - widest-line@3.1.0: + file:packages/holodeck(@ember-data/request@4.12.8)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/holodeck, type: directory} + id: file:packages/holodeck + name: '@warp-drive/holodeck' + engines: {node: '>= 18.20.4'} + hasBin: true + peerDependencies: + '@ember-data/request': workspace:* + '@warp-drive/core-types': workspace:* dependencies: - string-width: 4.2.3 + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@hono/node-server': 1.13.7(hono@4.6.9) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + chalk: 5.3.0 + hono: 4.6.9 + dev: true - word-wrap@1.2.3: {} - - wordwrap@0.0.3: {} + file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/json-api, type: directory} + id: file:packages/json-api + name: '@ember-data/json-api' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + dependencies: + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - wordwrap@1.0.0: {} + file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/json-api, type: directory} + id: file:packages/json-api + name: '@ember-data/json-api' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + dependencies: + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - workerpool@3.1.2: + file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/legacy-compat, type: directory} + id: file:packages/legacy-compat + name: '@ember-data/legacy-compat' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/json-api': workspace:* + '@ember-data/request': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@ember/test-waiters': ^3.1.0 + '@warp-drive/core-types': workspace:* + ember-source: '*' + peerDependenciesMeta: + '@ember-data/graph': + optional: true + '@ember-data/json-api': + optional: true dependencies: - '@babel/core': 7.24.5 - object-assign: 4.1.1 - rsvp: 4.8.5 + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) transitivePeerDependencies: + - '@babel/core' + - '@glint/template' - supports-color + dev: true - workerpool@6.2.1: {} - - workerpool@6.4.0: {} - - wrap-ansi@7.0.0: + file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/legacy-compat, type: directory} + id: file:packages/legacy-compat + name: '@ember-data/legacy-compat' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/json-api': workspace:* + '@ember-data/request': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@ember/test-waiters': ^3.1.0 + '@warp-drive/core-types': workspace:* + ember-source: '*' + peerDependenciesMeta: + '@ember-data/graph': + optional: true + '@ember-data/json-api': + optional: true dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - write-file-atomic@3.0.3: + file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/model, type: directory} + id: file:packages/model + name: '@ember-data/model' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/json-api': workspace:* + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@ember-data/tracking': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' + peerDependenciesMeta: + '@ember-data/graph': + optional: true + '@ember-data/json-api': + optional: true dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + inflection: 3.0.0 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - write-file-atomic@5.0.1: + file:packages/model(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/model, type: directory} + id: file:packages/model + name: '@ember-data/model' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/graph': workspace:* + '@ember-data/json-api': workspace:* + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@ember-data/tracking': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' + peerDependenciesMeta: + '@ember-data/graph': + optional: true + '@ember-data/json-api': + optional: true dependencies: - imurmurhash: 0.1.4 - signal-exit: 4.1.0 + '@ember-data/graph': file:packages/graph(@babel/core@7.26.0)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/json-api': file:packages/json-api(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8) + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + inflection: 3.0.0 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - write-yaml-file@5.0.0: + file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/request, type: directory} + id: file:packages/request + name: '@ember-data/request' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@warp-drive/core-types': workspace:* dependencies: - js-yaml: 4.1.0 - write-file-atomic: 5.0.1 - - ws@7.5.9: {} - - ws@8.11.0: {} - - ws@8.13.0: {} - - xdg-basedir@4.0.0: {} - - xml-name-validator@3.0.0: {} - - xml-name-validator@4.0.0: {} - - xmlchars@2.2.0: {} - - xtend@4.0.2: {} - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yallist@4.0.0: {} + '@ember/test-waiters': 3.1.0(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - yam@1.0.0: + file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0): + resolution: {directory: packages/request-utils, type: directory} + id: file:packages/request-utils + name: '@ember-data/request-utils' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/string': ^3.1.1 || ^4.0.0 + '@warp-drive/core-types': workspace:* + ember-inflector: ^4.0.2 || ^5.0.0 + ember-source: '*' + peerDependenciesMeta: + '@ember/string': + optional: true + ember-inflector: + optional: true dependencies: - fs-extra: 4.0.3 - lodash.merge: 4.6.2 + '@ember/string': 3.1.1(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-inflector: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - yargs-parser@20.2.4: {} + file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0): + resolution: {directory: packages/request-utils, type: directory} + id: file:packages/request-utils + name: '@ember-data/request-utils' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/string': ^3.1.1 || ^4.0.0 + '@warp-drive/core-types': workspace:* + ember-inflector: ^4.0.2 || ^5.0.0 + ember-source: '*' + peerDependenciesMeta: + '@ember/string': + optional: true + ember-inflector: + optional: true + dependencies: + '@ember/string': 3.1.1(@babel/core@7.26.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-inflector: 4.0.3(@babel/core@7.26.0)(ember-source@5.12.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - yargs-parser@21.1.1: {} + file:packages/request-utils(@babel/core@7.26.0)(@ember/string@4.0.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/request-utils, type: directory} + id: file:packages/request-utils + name: '@ember-data/request-utils' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember/string': ^3.1.1 || ^4.0.0 + '@warp-drive/core-types': workspace:* + ember-inflector: ^4.0.2 || ^5.0.0 + ember-source: '*' + peerDependenciesMeta: + '@ember/string': + optional: true + ember-inflector: + optional: true + dependencies: + '@ember/string': 4.0.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - yargs-unparser@2.0.0: + file:packages/rest(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8): + resolution: {directory: packages/rest, type: directory} + id: file:packages/rest + name: '@ember-data/rest' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request-utils': workspace:* + '@ember-data/store': ^4.12.0 || ^5.0.0 + '@warp-drive/core-types': workspace:* dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - yargs@16.2.0: + file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/serializer, type: directory} + id: file:packages/serializer + name: '@ember-data/serializer' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.4 + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - yargs@17.7.1: + file:packages/serializer(@babel/core@7.26.0)(@ember-data/legacy-compat@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/store@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/serializer, type: directory} + id: file:packages/serializer + name: '@ember-data/serializer' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/legacy-compat': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/store': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 + '@ember-data/legacy-compat': file:packages/legacy-compat(@babel/core@7.26.0)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember/test-waiters@3.1.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - yargs@17.7.2: + file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/store, type: directory} + id: file:packages/store + name: '@ember-data/store' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/tracking': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true - yocto-queue@0.1.0: {} + file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/store, type: directory} + id: file:packages/store + name: '@ember-data/store' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request': workspace:* + '@ember-data/request-utils': workspace:* + '@ember-data/tracking': workspace:* + '@warp-drive/core-types': workspace:* + ember-source: '*' + dependencies: + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/request-utils': file:packages/request-utils(@babel/core@7.26.0)(@ember/string@3.1.1)(@warp-drive/core-types@4.12.8)(ember-inflector@4.0.3)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - yui@3.18.1: + file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/tracking, type: directory} + id: file:packages/tracking + name: '@ember-data/tracking' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@warp-drive/core-types': workspace:* + ember-source: '*' dependencies: - request: 2.40.0 + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color - yuidocjs@0.10.2: + file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/unpublished-test-infra, type: directory} + id: file:packages/unpublished-test-infra + name: '@ember-data/unpublished-test-infra' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request': workspace:* + '@ember-data/store': workspace:* + '@ember-data/tracking': workspace:* + '@ember/test-helpers': 3.3.0 || ^4.0.4 + '@warp-drive/core-types': workspace:* + '@warp-drive/diagnostic': workspace:* + ember-source: '*' + testem: ~3.11.0 + peerDependenciesMeta: + '@warp-drive/diagnostic': + optional: true + testem: + optional: true dependencies: - express: 4.18.2 - graceful-fs: 4.2.11 - markdown-it: 4.4.0 - mdn-links: 0.1.0 - minimatch: 3.1.2 - rimraf: 2.7.1 - yui: 3.18.1 + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + chalk: 4.1.2 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + semver: 7.6.3 transitivePeerDependencies: + - '@babel/core' + - '@glint/template' - supports-color + dev: true - zlib@1.0.5: {} + file:packages/unpublished-test-infra(@babel/core@7.26.0)(@ember-data/request@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/test-helpers@4.0.4)(@warp-drive/core-types@4.12.8)(@warp-drive/diagnostic@4.12.8)(ember-source@5.12.0): + resolution: {directory: packages/unpublished-test-infra, type: directory} + id: file:packages/unpublished-test-infra + name: '@ember-data/unpublished-test-infra' + engines: {node: '>= 18.20.4'} + peerDependencies: + '@ember-data/request': workspace:* + '@ember-data/store': workspace:* + '@ember-data/tracking': workspace:* + '@ember/test-helpers': 3.3.0 || ^4.0.4 + '@warp-drive/core-types': workspace:* + '@warp-drive/diagnostic': workspace:* + ember-source: '*' + testem: ~3.11.0 + peerDependenciesMeta: + '@warp-drive/diagnostic': + optional: true + testem: + optional: true + dependencies: + '@ember-data/request': file:packages/request(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8) + '@ember-data/store': file:packages/store(@babel/core@7.26.0)(@ember-data/request-utils@4.12.8)(@ember-data/request@4.12.8)(@ember-data/tracking@4.12.8)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember-data/tracking': file:packages/tracking(@babel/core@7.26.0)(@glint/template@1.5.0)(@warp-drive/core-types@4.12.8)(ember-source@5.12.0) + '@ember/test-helpers': 4.0.4(patch_hash=zignhd6n3rugkiuawsmbuxfdka)(@babel/core@7.26.0)(@glint/template@1.5.0)(ember-source@5.12.0) + '@embroider/macros': 1.16.9(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/build-config': file:packages/build-config(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/core-types': file:packages/core-types(@babel/core@7.26.0)(@glint/template@1.5.0) + '@warp-drive/diagnostic': file:packages/diagnostic(@babel/core@7.26.0)(@ember/test-helpers@4.0.4)(ember-cli-test-loader@3.1.0)(ember-source@5.12.0) + chalk: 4.1.2 + ember-source: 5.12.0(@glimmer/component@1.1.2)(@glint/template@1.5.0) + qunit: 2.19.4(patch_hash=2jwk2nz4gqke2k5hv6ptj42llu) + semver: 7.6.3 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6f5f1562d6..bcfbc43e1e3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - - 'packages/*' - - 'tests/*' + - "packages/*" + - "tests/*" + - "config" diff --git a/release/core/latest-for/index.ts b/release/core/latest-for/index.ts new file mode 100644 index 00000000000..5567d5f3b10 --- /dev/null +++ b/release/core/latest-for/index.ts @@ -0,0 +1,8 @@ +import { NPM_DIST_TAG } from '../../utils/channel'; +import { getPublishedChannelInfo } from '../../utils/git'; + +export async function latestFor(args: string[]) { + const channel = args[0] as NPM_DIST_TAG; + const version = (await getPublishedChannelInfo({ silent: true }))[channel]; + console.log(version); +} diff --git a/release/core/promote/index.ts b/release/core/promote/index.ts new file mode 100644 index 00000000000..716fa453250 --- /dev/null +++ b/release/core/promote/index.ts @@ -0,0 +1,137 @@ +import { promote_flags_config } from '../../utils/flags-config'; +import { parseRawFlags } from '../../utils/parse-args'; +import { GIT_TAG, getAllPackagesForGitTag, getGitState, pushLTSTagToRemoteBranch } from '../../utils/git'; +import { printHelpDocs } from '../../help/docs'; +import { Package } from '../../utils/package'; +import { SEMVER_VERSION } from '../../utils/channel'; +import chalk from 'chalk'; +import { colorName } from '../publish/steps/print-strategy'; +import { exec } from '../../utils/cmd'; +import { question } from '../publish/steps/confirm-strategy'; + +export async function promoteToLTS(args: string[]) { + // get user supplied config + const config = await parseRawFlags(args, promote_flags_config); + const gitTag: GIT_TAG = `v${config.full.get('version') as SEMVER_VERSION}`; + + if (config.full.get('help')) { + return printHelpDocs(args); + } + + const packages = await getAllPackagesForGitTag(gitTag); + const versionsToPromote = getPublicPackageVersions(packages); + + await updateTags(config.full, versionsToPromote); + + if (config.full.get('upstream') && !config.full.get('dry_run')) { + try { + await pushLTSTagToRemoteBranch(gitTag, true); + } catch (e) { + console.error(chalk.red(`NPM Tag Updated, but failed to update the remote lts branch for ${gitTag}`)); + console.error(e); + } + } +} + +export function getPublicPackageVersions(packages: Map): Map { + const publicPackages = new Map(); + packages.forEach((pkg, name) => { + if (!pkg.pkgData.private) { + publicPackages.set(name, pkg.pkgData.version); + } + }); + return publicPackages; +} + +export async function updateTags( + config: Map, + packages: Map +) { + const distTag = config.get('tag') as string; + const NODE_AUTH_TOKEN = process.env.NODE_AUTH_TOKEN; + const CI = process.env.CI; + let token: string | undefined; + + // allow OTP token usage locally + if (!NODE_AUTH_TOKEN) { + if (CI) { + console.log( + chalk.red( + '🚫 NODE_AUTH_TOKEN not found in ENV. NODE_AUTH_TOKEN is required in ENV to publish from CI. Exiting...' + ) + ); + process.exit(1); + } + token = await getOTPToken(distTag); + } else { + if (!CI) { + const result = await question( + `\n${chalk.cyan('NODE_AUTH_TOKEN')} found in ENV.\nPublish ${config.get('increment')} release in ${config.get( + 'channel' + )} channel to the ${config.get('tag')} tag on the npm registry? ${chalk.yellow('[y/n]')}:` + ); + const input = result.trim().toLowerCase(); + if (input !== 'y' && input !== 'yes') { + console.log(chalk.red('🚫 Publishing not confirmed. Exiting...')); + process.exit(1); + } + } + } + + const dryRun = config.get('dry_run') as boolean; + + for (const [pkgName, version] of packages) { + token = await updateDistTag(pkgName, version, distTag, dryRun, token); + console.log(chalk.green(`\t✅ ${colorName(pkgName)} ${chalk.green(version)} => ${chalk.magenta(distTag)}`)); + } + + console.log( + `✅ ` + chalk.cyan(`Moved ${chalk.greenBright(packages.size)} 📦 packages to ${chalk.magenta(distTag)} channel`) + ); +} + +async function getOTPToken(distTag: string, reprompt?: boolean) { + const prompt = reprompt + ? `The provided OTP token has expired. Please enter a new OTP token: ` + : `\nℹ️ ${chalk.cyan( + 'NODE_AUTH_TOKEN' + )} not found in ENV.\n\nConfiguring NODE_AUTH_TOKEN is the preferred mechanism by which to publish. Alternatively you may continue using an OTP token.\n\nUpdating ${distTag} tag on the npm registry.\n\nEnter your OTP token: `; + + let token = await question(prompt); + + return token.trim(); +} + +export async function updateDistTag( + pkg: string, + version: string, + distTag: string, + dryRun: boolean, + otp?: string +): Promise { + let cmd = `npm dist-tag add ${pkg}@${version} ${distTag}`; + + if (otp) { + cmd += ` --otp=${otp}`; + } + + if (dryRun) { + cmd += ' --dry-run'; + } + + try { + await exec({ cmd, condense: true }); + } catch (e) { + if (!otp || !(e instanceof Error)) { + throw e; + } + if (e.message.includes('E401') || e.message.includes('EOTP')) { + otp = await getOTPToken(distTag, true); + return updateDistTag(pkg, version, distTag, dryRun, otp); + } else { + throw e; + } + } + + return otp; +} diff --git a/release/core/publish/index.ts b/release/core/publish/index.ts new file mode 100644 index 00000000000..d98917f30a3 --- /dev/null +++ b/release/core/publish/index.ts @@ -0,0 +1,85 @@ +import { publish_flags_config } from '../../utils/flags-config'; +import { parseRawFlags } from '../../utils/parse-args'; +import { GIT_TAG, getAllPackagesForGitTag, getGitState } from '../../utils/git'; +import { printHelpDocs } from '../../help/docs'; +import { bumpAllPackages, restorePackagesForDryRun } from './steps/bump-versions'; +import { generatePackageTarballs } from './steps/generate-tarballs'; +import { generateMirrorTarballs } from './steps/generate-mirror-tarballs'; +import { printStrategy } from './steps/print-strategy'; +import { AppliedStrategy, applyStrategy } from './steps/generate-strategy'; +import { confirmStrategy } from './steps/confirm-strategy'; +import { publishPackages } from './steps/publish-packages'; +import { gatherPackages, loadStrategy } from '../../utils/package'; +import { CHANNEL, SEMVER_VERSION } from '../../utils/channel'; +import { confirmCommitChangelogs } from '../release-notes/steps/confirm-changelogs'; +import { updateChangelogs } from '../release-notes/steps/update-changelogs'; +import { getChanges } from '../release-notes/steps/get-changes'; +import { generateTypesTarballs } from './steps/generate-types-tarballs'; + +export async function executePublish(args: string[]) { + // get user supplied config + const config = await parseRawFlags(args, publish_flags_config); + + if (config.full.get('help')) { + return printHelpDocs(args); + } + + const dryRun = config.full.get('dry_run') as boolean; + + // get git info + await getGitState(config.full); + + // get configured strategy + const strategy = await loadStrategy(); + + // get packages present on our current branch + const packages = await gatherPackages(strategy.config); + + // get packages present in the git tag version + const fromVersion = config.full.get('from') as SEMVER_VERSION | undefined; + const fromTag = `v${fromVersion}` as GIT_TAG; + const baseVersionPackages = fromVersion ? await getAllPackagesForGitTag(fromTag) : packages; + + // get applied strategy + const applied = await applyStrategy(config.full, strategy, baseVersionPackages, packages); + + // print strategy to be applied + await printStrategy(config.full, applied); + + await confirmStrategy(); + + const channel = config.full.get('channel') as CHANNEL; + if (channel !== 'canary' && channel !== 'beta') { + // generate the list of changes + const newChanges = await getChanges(strategy, packages, fromTag); + + // update all changelogs, including the primary changelog + // and the changelogs for each package in changelogRoots + // this will not commit the changes + const changedFiles = await updateChangelogs(fromTag, newChanges, config.full, strategy, packages, applied); + + await confirmCommitChangelogs(changedFiles, config.full, applied); + } + + // Bump package.json versions & commit/tag + // ======================== + await bumpAllPackages(config.full, packages, applied.all); + + if (dryRun) await restorePackagesForDryRun(packages, applied.all); + + // Generate Tarballs in tmp/tarballs/ + // Having applied the types publishing strategy "just in time" + // ======================== + if (config.full.get('pack')) { + await generatePackageTarballs(config.full, packages, applied.public_pks); + await generateMirrorTarballs(config.full, packages, applied.public_pks); + await generateTypesTarballs(config.full, packages, applied.public_pks); + } else { + console.log(`Skipped Pack`); + } + + // Publish to NPM registry + // ======================== + if (config.full.get('publish')) await publishPackages(config.full, packages, applied.public_pks); + else console.log(`Skipped Publish`); +} diff --git a/release/core/publish/steps/bump-versions.ts b/release/core/publish/steps/bump-versions.ts new file mode 100644 index 00000000000..6926bf2ef4a --- /dev/null +++ b/release/core/publish/steps/bump-versions.ts @@ -0,0 +1,100 @@ +import chalk from 'chalk'; +import { exec } from '../../../utils/cmd'; +import { APPLIED_STRATEGY, Package } from '../../../utils/package'; + +/** + * This function will consume the strategy, bump the versions of all packages, + * and then commit the changes. This includes updating the project lockfile. + * + * The changes will be committed with a message of "Release v${nextVersion}" + * where nextVersion is the version that the root package will be bumped to. + * + * The tag `v${nextVersion}` will be created to match. + * + * @internal + */ +export async function bumpAllPackages( + config: Map, + packages: Map, + strategy: Map +) { + for (const [, pkg] of packages) { + const strat = strategy.get(pkg.pkgData.name); + if (!strat) { + throw new Error(`Unable to find strategy for package ${pkg.pkgData.name}`); + } + pkg.pkgData.version = strat.toVersion; + + // update any referenced packages in dependencies + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.dependencies || {}, strategy); + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.devDependencies || {}, strategy); + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.peerDependencies || {}, strategy); + + await pkg.file.write(); + } + + const willPublish: boolean = Boolean(config.get('pack') && config.get('publish')); + const dryRun = config.get('dry_run') as boolean; + const nextVersion = strategy.get('root')?.toVersion; + let commitCommand = `git commit -am "Release v${nextVersion}"`; + + if (!dryRun) { + if (willPublish) commitCommand = `pnpm install --no-frozen-lockfile && ` + commitCommand; + else commitCommand = `pnpm install && ` + commitCommand; + commitCommand += ` && git tag v${nextVersion}`; + } + + // Let the github action determine whether to push the tag to remote + if (!dryRun && config.get('upstream')) { + commitCommand += ` && git push && git push origin v${nextVersion}`; + } + + const cleanCommand = willPublish ? `git clean -fdx && ` : ''; + const finalCommand = process.env.CI + ? ['sh', '-c', `${cleanCommand}${commitCommand}`] + : ['zsh', '-c', `${cleanCommand}${commitCommand}`]; + + await exec(finalCommand, dryRun); + console.log(`✅ ` + chalk.cyan(`Successfully Versioned ${nextVersion}`)); +} + +function bumpKnownProjectVersionsFromStrategy( + deps: Record, + strategy: Map, + restore = false +) { + let changed = false; + Object.keys(deps).forEach((depName) => { + const strat = strategy.get(depName); + if (!strat) { + return; + } + if (deps[depName].startsWith('workspace:')) { + deps[depName] = `workspace:${restore ? strat.fromVersion : strat.toVersion}`; + changed = true; + } + }); + return changed; +} + +export async function restorePackagesForDryRun( + packages: Map, + strategy: Map +) { + for (const [, pkg] of packages) { + const strat = strategy.get(pkg.pkgData.name); + if (!strat) { + throw new Error(`Unable to find strategy for package ${pkg.pkgData.name}`); + } + pkg.pkgData.version = strat.fromVersion; + + // update any referenced packages in dependencies + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.dependencies || {}, strategy, true); + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.devDependencies || {}, strategy, true); + bumpKnownProjectVersionsFromStrategy(pkg.pkgData.peerDependencies || {}, strategy, true); + + await pkg.file.write(); + } + + console.log(`\t♻️ ` + chalk.grey(`Successfully Restored Versions for DryRun`)); +} diff --git a/release/core/publish/steps/confirm-strategy.ts b/release/core/publish/steps/confirm-strategy.ts new file mode 100644 index 00000000000..bd729e531b3 --- /dev/null +++ b/release/core/publish/steps/confirm-strategy.ts @@ -0,0 +1,41 @@ +import chalk from 'chalk'; +import * as readline from 'readline/promises'; + +export async function confirmStrategy() { + return confirm({ + prompt: chalk.white(`\nDo you want to continue with this strategy?`), + cancelled: chalk.red('🚫 Strategy not confirmed. Exiting...'), + }); +} + +/** + * Prompt user to continue, exit if not confirmed. + * + * In CI environments, this function will return immediately without prompting. + * + * config.prompt - The prompt to display to the user + * config.cancelled - The message to display if the user cancels + * + * yes/no prompt will be added to the end of the prompt text automatically. + * + * @internal + */ +export async function confirm(config: { prompt: string; cancelled: string }): Promise { + if (process.env.CI) { + return; + } + const confirm = await question(`${config.prompt} ${chalk.yellow(`[y/n]`)}: `); + const input = confirm.trim().toLowerCase(); + if (input !== 'y' && input !== 'yes') { + console.log(config.cancelled); + process.exit(1); + } +} + +export async function question(prompt: string) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return rl.question(prompt); +} diff --git a/release/core/publish/steps/generate-mirror-tarballs.ts b/release/core/publish/steps/generate-mirror-tarballs.ts new file mode 100644 index 00000000000..4b6d96c0c1a --- /dev/null +++ b/release/core/publish/steps/generate-mirror-tarballs.ts @@ -0,0 +1,102 @@ +import { PROJECT_ROOT, TARBALL_DIR, toTarballName } from './generate-tarballs'; + +import { Glob } from 'bun'; +import { exec } from '../../../utils/cmd'; +import path from 'path'; +import fs from 'fs'; +import { APPLIED_STRATEGY, Package } from '../../../utils/package'; + +export async function generateMirrorTarballs( + config: Map, + packages: Map, + strategy: Map +) { + const tarballDir = path.join(TARBALL_DIR, packages.get('root')!.pkgData.version); + const tmpDir = path.join(PROJECT_ROOT, 'tmp/unpacked', packages.get('root')!.pkgData.version); + fs.mkdirSync(tmpDir, { recursive: true }); + + // for each public package + // if the package should be mirrored + // generate a tarball + // + // to do this we need to: + // - unpack the main tarball for the package + // - replace any references to the original package names with the mirrored package names in every file + // - pack a new tarball into the tarballs directory + // - add the tarball path to the package object + const toReplace = new Map(); + const cautionReplace = new Map(); + for (const [, strat] of strategy) { + if (strat.mirrorPublish) { + if (!strat.name.startsWith('@')) { + cautionReplace.set(strat.name, strat.mirrorPublishTo); + } else { + toReplace.set(strat.name, strat.mirrorPublishTo); + } + } + } + + for (const [, strat] of strategy) { + if (strat.mirrorPublish) { + const pkg = packages.get(strat.name)!; + const mirrorTarballPath = path.join( + tarballDir, + `${toTarballName(strat.mirrorPublishTo)}-${pkg.pkgData.version}.tgz` + ); + + // unpack the main tarball for the package + const mainTarballPath = pkg.tarballPath; + const unpackedDir = path.join(tmpDir, pkg.pkgData.name); + const realUnpackedDir = path.join(unpackedDir, 'package'); + + fs.mkdirSync(unpackedDir, { recursive: true }); + await exec(`tar -xf ${mainTarballPath} -C ${unpackedDir}`); + + // replace any references to the original package names with the mirrored package names in every file + // to do this we scan every file in the unpacked directory and do a string replace + const glob = new Glob('**/*'); + + for await (const filePath of glob.scan(realUnpackedDir)) { + const fullPath = path.join(realUnpackedDir, filePath); + const file = Bun.file(fullPath); + const fileData = await file.text(); + + let newContents = fileData; + for (const [from, to] of toReplace) { + newContents = newContents.replace(new RegExp(from, 'g'), to); + } + for (const [from, to] of cautionReplace) { + newContents = newContents.replace(new RegExp(`'${from}`, 'g'), `'${to}`); + newContents = newContents.replace(new RegExp(`"${from}`, 'g'), `"${to}`); + } + + newContents = newContents.replace(new RegExp(`'@ember-data/'`, 'g'), `'@ember-data-mirror/'`); + newContents = newContents.replace(new RegExp(`"@ember-data/"`, 'g'), `"@ember-data-mirror/"`); + + await Bun.write(fullPath, newContents); + } + + // fix the volta extends field in package.json + const packageJsonPath = path.join(realUnpackedDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.volta && packageJson.volta.extends) { + if (packageJson.name.includes('/')) { + packageJson.volta.extends = '../../../../../../package.json'; + } else { + packageJson.volta.extends = '../../../../../package.json'; + } + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + } + + // pack the new package and put it in the tarballs directory + const result = await exec({ + cwd: realUnpackedDir, + cmd: `npm pack --pack-destination=${tarballDir}`, + condense: false, + }); + console.log(result); + + pkg.mirrorTarballPath = mirrorTarballPath; + } + } +} diff --git a/release/core/publish/steps/generate-strategy.ts b/release/core/publish/steps/generate-strategy.ts new file mode 100644 index 00000000000..fdec96295d8 --- /dev/null +++ b/release/core/publish/steps/generate-strategy.ts @@ -0,0 +1,164 @@ +import chalk from 'chalk'; +import { CHANNEL, npmDistTagForChannelAndVersion } from '../../../utils/channel'; + +import { APPLIED_STRATEGY, Package, STRATEGY } from '../../../utils/package'; +import { getNextVersion } from '../../utils/next-version'; +import path from 'path'; +import semver from 'semver'; + +const PROJECT_ROOT = process.cwd(); + +function sortByName(map: Map) { + const sorted = [...map.values()]; + sorted.sort((a, b) => { + if (a.name.startsWith('@') && !b.name.startsWith('@')) { + return 1; + } + + if (!a.name.startsWith('@') && b.name.startsWith('@')) { + return -1; + } + return a.name > b.name ? 1 : -1; + }); + map.clear(); + sorted.forEach((v) => { + map.set(v.name, v); + }); +} + +function getMirrorPackageName(name: string) { + if (name === 'root') { + return 'N/A'; + } + if (name.startsWith('@ember-data/')) { + return name.replace('@ember-data/', '@ember-data-mirror/'); + } else if (name.startsWith('@warp-drive/')) { + return name.replace('@warp-drive/', '@warp-drive-mirror/'); + } else if (name.startsWith('ember-data')) { + return name.replace('ember-data', 'ember-data-mirror'); + } + throw new Error(`Could not determine mirror package name for ${name}`); +} + +function getTypesPackageName(name: string) { + if (name === 'root') { + return 'N/A'; + } + if (name.startsWith('@ember-data/')) { + return name.replace('@ember-data/', '@ember-data-types/'); + } else if (name.startsWith('@warp-drive/')) { + return name.replace('@warp-drive/', '@warp-drive-types/'); + } else if (name.startsWith('ember-data')) { + return name.replace('ember-data', 'ember-data-types'); + } + throw new Error(`Could not determine types package name for ${name}`); +} + +function getPkgDir(pkgFilePath: string) { + const relative = path.relative(PROJECT_ROOT, pkgFilePath); + const parts = relative.split('/'); + if (parts.length === 1) { + return ''; + } + return '/' + parts[0]; +} + +export async function applyStrategy( + config: Map, + strategy: STRATEGY, + baseVersionPackages: Map, + toPackages: Map = baseVersionPackages +): Promise { + const channel = config.get('channel') as CHANNEL; + const increment = config.get('increment') as 'major' | 'minor' | 'patch'; + const applied_strategies = new Map(); + const private_pkgs = new Map(); + const public_pks = new Map(); + const isReversion = baseVersionPackages !== toPackages; + const newBaseVersion = baseVersionPackages.get('root')!.pkgData.version; + const currentVersion = toPackages.get('root')!.pkgData.version; + // if we are downversioning, then the currentVersion root will have a higher version than the newBaseVersion + // + // a downversion occurs when for instance we decide to release a new stable patch from current beta or main + const isDownversion = isReversion && semver.gt(currentVersion, newBaseVersion); + if (isDownversion) { + console.log( + `\n\n\t==========================================\n\t⚠️\t${chalk.yellow( + 'Down-Versioning Detected:' + )}\n\t\tConverting primary version from ${chalk.greenBright(currentVersion)} to ${chalk.greenBright( + newBaseVersion + )} before applying strategy for ${increment} bump.\n\n\t\tAlpha and Beta packages will be marked as private.\n\t==========================================\n` + ); + } + + for (const [name, pkg] of toPackages) { + const rule = strategy.rules[name] || strategy.defaults; + const applied_strategy = Object.assign({}, rule) as APPLIED_STRATEGY; + const fromPkg = baseVersionPackages.get(name); + + applied_strategy.name = name; + applied_strategy.private = Boolean(pkg.pkgData.private); + applied_strategy.pkgDir = getPkgDir(pkg.filePath); + applied_strategy.fromVersion = fromPkg ? fromPkg.pkgData.version : pkg.pkgData.version; + applied_strategy.new = !fromPkg; + applied_strategy.mirrorPublish = + !applied_strategy.private && (rule.mirrorPublish ?? strategy.defaults.mirrorPublish ?? false); + applied_strategy.typesPublish = + !applied_strategy.private && (rule.typesPublish ?? strategy.defaults.typesPublish ?? false); + applied_strategy.mirrorPublishTo = applied_strategy.mirrorPublish ? getMirrorPackageName(name) : 'N/A'; + applied_strategy.typesPublishTo = applied_strategy.typesPublish ? getTypesPackageName(name) : 'N/A'; + + if (isDownversion) { + // during a downversion, we do not allow publishing a package whose current strategy is + // alpha or beta. + // this is because any version bump could conflict with the version in the canary channel. + // so for instance, if we have canary of an alpha project at 0.0.1-alpha.5, + // any downversion bump would result in 0.0.1 + // but if we were to downversion the primary version from say 5.4.0-alpha.1 to both 5.3.1 and 5.2.1, + // then we would have a conflict as both would try to publish the alpha version at 0.0.1 + if (rule.stage === 'alpha' || rule.stage === 'beta') { + applied_strategy.private = true; // always mark as private to avoid a new publish + applied_strategy.toVersion = pkg.pkgData.version; // preserve the existing version + pkg.pkgData.private = true; // mark the package as private, we will save this when applying version changes later + } + + // handle packages that didn't exist in the fromPackages + else if (!fromPkg && rule.stage === 'stable') { + if (pkg.pkgData.version === currentVersion) { + applied_strategy.fromVersion = newBaseVersion; + } + + applied_strategy.toVersion = getNextVersion(applied_strategy.fromVersion, channel, increment, rule.stage); + } else { + applied_strategy.toVersion = getNextVersion(applied_strategy.fromVersion, channel, increment, rule.stage); + } + } else { + applied_strategy.toVersion = getNextVersion(applied_strategy.fromVersion, channel, increment, rule.stage); + } + + // channels may not change outside of a major or minor bump + // major and minor bumps may only occur on beta|canary|release|lts + // and never lts-* or release-* and so existing fromVersion is safe + // to use. + applied_strategy.distTag = npmDistTagForChannelAndVersion(channel, applied_strategy.fromVersion); + applied_strategies.set(name, applied_strategy); + + applied_strategy.private ? private_pkgs.set(name, applied_strategy) : public_pks.set(name, applied_strategy); + } + + sortByName(applied_strategies); + sortByName(private_pkgs); + sortByName(public_pks); + + return { + all: applied_strategies, + private_pkgs, + public_pks, + }; +} + +export type AppliedStrategy = { + all: Map; + private_pkgs: Map; + public_pks: Map; +}; diff --git a/release/core/publish/steps/generate-tarballs.ts b/release/core/publish/steps/generate-tarballs.ts new file mode 100644 index 00000000000..529d4535a09 --- /dev/null +++ b/release/core/publish/steps/generate-tarballs.ts @@ -0,0 +1,422 @@ +import chalk from 'chalk'; +import { exec } from '../../../utils/cmd'; +import { APPLIED_STRATEGY, Package } from '../../../utils/package'; +import path from 'path'; +import fs from 'fs'; +import { Glob } from 'bun'; + +export const PROJECT_ROOT = process.cwd(); +export const TARBALL_DIR = path.join(PROJECT_ROOT, 'tmp/tarballs'); + +export function toTarballName(name: string) { + return name.replace('@', '').replace('/', '-'); +} + +/** + * Iterates the public packages declared in the strategy and + * generates tarballs in the tmp/tarballs/ directory. + * + * @internal + */ +export async function generatePackageTarballs( + config: Map, + packages: Map, + strategy: Map +) { + // ensure tarball directory exists + const tarballDir = path.join(TARBALL_DIR, packages.get('root')!.pkgData.version); + fs.mkdirSync(tarballDir, { recursive: true }); + + // first loop executes build steps for each package so that entangled + // builds always have access to everything they need + for (const [, pkgStrategy] of strategy) { + const pkg = packages.get(pkgStrategy.name)!; + if (pkg.pkgData.private) { + throw new Error(`Unexpected attempt to publish private package ${pkg.pkgData.name}`); + } + + if (!Array.isArray(pkg.pkgData.files) || pkg.pkgData.files.length === 0) { + throw new Error(`Unexpected attempt to publish package ${pkg.pkgData.name} with no files`); + } + + try { + if (pkg.pkgData.scripts?.['prepack']) { + await exec({ cwd: path.join(PROJECT_ROOT, path.dirname(pkg.filePath)), cmd: `bun run prepack` }); + } + } catch (e) { + console.log(`🔴 ${chalk.redBright('failed to execute prepack script for')} ${chalk.yellow(pkg.pkgData.name)}`); + throw e; + } + } + + // second loop cleans up and packs each package + for (const [, pkgStrategy] of strategy) { + const pkg = packages.get(pkgStrategy.name)!; + + try { + await fixVersionsInPackageJson(pkg); + await amendFilesForTypesStrategy(pkg, pkgStrategy); + } catch (e) { + console.log(`🔴 ${chalk.redBright('failed to amend files to pack for')} ${chalk.yellow(pkg.pkgData.name)}`); + throw e; + } + + try { + const pkgDir = path.join(PROJECT_ROOT, path.dirname(pkg.filePath)); + const tarballPath = path.join(tarballDir, `${toTarballName(pkg.pkgData.name)}-${pkg.pkgData.version}.tgz`); + pkg.tarballPath = tarballPath; + const result = await exec({ cwd: pkgDir, cmd: `npm pack --pack-destination=${tarballDir}`, condense: false }); + console.log(result); + } catch (e) { + console.log(`🔴 ${chalk.redBright('failed to generate tarball for')} ${chalk.yellow(pkg.pkgData.name)}`); + throw e; + } finally { + // restore state from before amending for types strategy + await restoreTypesStrategyChanges(pkg, pkgStrategy); + } + } + + console.log( + `✅ ` + + chalk.cyan( + `created ${chalk.greenBright(strategy.size)} 📦 tarballs in ${path.relative(PROJECT_ROOT, tarballDir)}` + ) + ); +} + +async function fixVersionsInPackageJson(pkg: Package) { + if (pkg.pkgData.dependencies) { + Object.keys(pkg.pkgData.dependencies).forEach((dep) => { + const version = pkg.pkgData.dependencies![dep]; + if (version.startsWith('workspace:')) { + pkg.pkgData.dependencies![dep] = version.replace('workspace:', ''); + } + }); + } + + if (pkg.pkgData.devDependencies) { + Object.keys(pkg.pkgData.devDependencies).forEach((dep) => { + const version = pkg.pkgData.devDependencies![dep]; + if (version.startsWith('workspace:')) { + pkg.pkgData.devDependencies![dep] = version.replace('workspace:', ''); + } + }); + } + + if (pkg.pkgData.peerDependencies) { + Object.keys(pkg.pkgData.peerDependencies).forEach((dep) => { + const version = pkg.pkgData.peerDependencies![dep]; + if (version.startsWith('workspace:')) { + pkg.pkgData.peerDependencies![dep] = version.replace('workspace:', ''); + } + }); + } + + await pkg.file.write(true); +} + +const PotentialTypesDirectories = new Set([ + 'unstable-preview-types', // alpha + 'preview-types', // beta + 'types', // stable +]); + +/** + * scrub the package.json of any types fields in exports + * to support private/alpha/beta types strategies + * + * @internal + */ +function scrubTypesFromExports(pkg: Package) { + // when addon is still V1, we completely remove the exports field + // to avoid issues with embroider, auto-import and v1 addons + if (pkg.pkgData['ember-addon']?.version === 1) { + delete pkg.pkgData.exports; + return; + } + + // scrub the package.json of any types fields in exports + if (pkg.pkgData.exports) { + // level 1 + for (const [key, value] of Object.entries(pkg.pkgData.exports)) { + if (key === 'types') { + delete pkg.pkgData.exports[key]; + } else if (typeof value === 'object') { + // level 2 + delete value.types; + + for (const [k, v] of Object.entries(value)) { + if (typeof v === 'object') { + // level 3 + delete v.types; + } + } + } + } + } +} + +async function makeTypesPrivate(pkg: Package) { + scrubTypesFromExports(pkg); + + // deactivate build types command + if (pkg.pkgData.scripts?.['build:types']) { + pkg.pkgData.scripts['build:types'] = 'echo "Types are private" && exit 0'; + } + + // and remove any types files from the published package artifacts + pkg.pkgData.files = pkg.pkgData.files?.filter((f) => { + return !PotentialTypesDirectories.has(f); + }); +} + +// convert each file to a module +// and write it back to the file system +// e.g. +// ``` +// declare module '@ember-data/model' { +// export default class Model {} +// } +// ``` +// +// instead of +// ``` +// export default class Model {} +// ``` +// +// additionally, rewrite each relative import +// to an absolute import +// e.g. if the types for @ember-data/model contain a file with +// the following import statement in the types directory +// +// ``` +// import attr from './attr'; +// ``` +// +// then it becomes +// +// ``` +// import attr from '@ember-data/model/attr'; +// ``` +async function convertFileToModule(fileData: string, relativePath: string, pkgName: string): Promise { + const lines = fileData.split('\n'); + const maybeModuleName = pkgName + '/' + relativePath.replace(/\.d\.ts$/, ''); + const moduleDir = pkgName + '/' + path.dirname(relativePath); + const moduleName = maybeModuleName.endsWith('/index') ? maybeModuleName.slice(0, -6) : maybeModuleName; + + for (let i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(/^declare /, '').replaceAll(' declare ', ' '); + const line = lines[i]; + + const isDynamicDoubleQuote = line.includes(`import(".`); + const isDynamicSingleQuote = line.includes(`import('.`); + if (isDynamicDoubleQuote || isDynamicSingleQuote) { + const matcher = isDynamicDoubleQuote ? /import\("([^"]+)"\)/ : /import\('([^']+)'\)/; + const importPath = line.match(matcher)![1]; + const newImportPath = path.join(moduleDir, importPath); + lines[i] = line.replace(importPath, newImportPath); + } else if (line.startsWith('import ')) { + if (!line.includes(`'`)) { + throw new Error(`Unhandled Import in ${relativePath}`); + } + if (line.includes(`'.`)) { + const importPath = line.match(/'([^']+)'/)![1]; + const newImportPath = path.join(moduleDir, importPath); + lines[i] = line.replace(importPath, newImportPath); + } + } + + // fix re-exports + else if (line.startsWith('export {') || line.startsWith('export type {')) { + if (!line.includes('}')) { + throw new Error(`Unhandled Re-export in ${relativePath}`); + } + if (line.includes(`'.`)) { + const importPath = line.match(/'([^']+)'/)![1]; + const newImportPath = path.join(moduleDir, importPath); + lines[i] = line.replace(importPath, newImportPath); + } + } + + // fix * re-exports + else if (line.startsWith('export * from')) { + if (!line.includes(`'`)) { + throw new Error(`Unhandled Re-export in ${relativePath}`); + } + if (line.includes(`'.`)) { + const importPath = line.match(/'([^']+)'/)![1]; + const newImportPath = path.join(moduleDir, importPath); + lines[i] = line.replace(importPath, newImportPath); + } + } + + // insert 2 spaces at the beginning of each line + // to account for module wrapper + if (!lines[i].startsWith('//# sourceMappingURL=')) lines[i] = ' ' + lines[i]; + } + + lines.unshift(`declare module '${moduleName}' {`); + const srcMapLine = lines.at(-1)!; + if (!srcMapLine.startsWith('//# sourceMappingURL=')) { + lines.push('}'); + } else { + lines.splice(-1, 0, '}'); + } + + const updatedFileData = lines.join('\n'); + + return updatedFileData; +} + +async function convertTypesToModules(pkg: Package, subdir: 'unstable-preview-types' | 'preview-types' | 'types') { + const typesDir = path.join(path.dirname(pkg.filePath), subdir); + const glob = new Glob('**/*.d.ts'); + + // we will insert a reference to each file in the index.d.ts + // so that all modules are available to consumers + // as soon as the tsconfig sources the types directory + const references = new Set(); + + // convert each file to a module + for await (const filePath of glob.scan(typesDir)) { + const fullPath = path.join(typesDir, filePath); + const file = Bun.file(fullPath); + const fileData = await file.text(); + const updatedFileData = await convertFileToModule(fileData, filePath, pkg.pkgData.name); + + if (filePath !== 'index.d.ts') { + references.add(`/// `); + } + + await Bun.write(file, updatedFileData); + } + + // write the references into the index.d.ts + const indexFile = Bun.file(path.join(typesDir, 'index.d.ts')); + const exists = await indexFile.exists(); + if (!exists) { + await Bun.write(indexFile, Array.from(references).join('\n')); + } else { + const fileData = await indexFile.text(); + const updatedFileData = Array.from(references).join('\n') + '\n' + fileData; + await Bun.write(indexFile, updatedFileData); + } +} + +async function makeTypesAlpha(pkg: Package) { + scrubTypesFromExports(pkg); + + // enforce that the correct types directory is present + const present = new Set(pkg.pkgData.files); + if (!present.has('unstable-preview-types')) { + throw new Error( + `Missing unstable-preview-types directory from published files for ${pkg.pkgData.name}. This package is using an alpha types strategy, and should thus publish an unstable-preview-types directory.` + ); + } + if (present.has('preview-types')) { + throw new Error( + `Unexpected preview-types directory in published files for ${pkg.pkgData.name}. This package is using an alpha types strategy, and should thus publish an unstable-preview-types directory.` + ); + } + if (present.has('types')) { + throw new Error( + `Unexpected types directory in published files for ${pkg.pkgData.name}. This package is using an alpha types strategy, and should thus publish an unstable-preview-types directory.` + ); + } + + await convertTypesToModules(pkg, 'unstable-preview-types'); + + // TODO we should probably scan our dist/addon directories for ts/.d.ts files and throw if found. +} + +async function makeTypesBeta(pkg: Package) { + scrubTypesFromExports(pkg); + + // enforce that the correct types directory is present + const present = new Set(pkg.pkgData.files); + if (!present.has('preview-types')) { + throw new Error( + `Missing preview-types directory from published files for ${pkg.pkgData.name}. This package is using a beta types strategy, and should thus publish a preview-types directory.` + ); + } + if (present.has('unstable-preview-types')) { + throw new Error( + `Unexpected unstable-preview-types directory in published files for ${pkg.pkgData.name}. This package is using a beta types strategy, and should thus publish a preview-types directory.` + ); + } + if (present.has('types')) { + throw new Error( + `Unexpected types directory in published files for ${pkg.pkgData.name}. This package is using a beta types strategy, and should thus publish a preview-types directory.` + ); + } + + // TODO we should probably scan our dist/addon directories for ts/.d.ts files and throw if found. +} +async function makeTypesStable(pkg: Package) { + // for stable, we expect that the types are automatically included + // so we check to ensure that types are in exports + if (!pkg.pkgData.exports) { + throw new Error( + `Missing exports field in package.json for ${pkg.pkgData.name}. This package is using a stable types strategy, and should thus include a types field in its exports.` + ); + } + const value = JSON.stringify(pkg.pkgData.exports); + if (!value.includes('types')) { + throw new Error( + `Missing types field in exports in package.json for ${pkg.pkgData.name}. This package is using a stable types strategy, and should thus include a types field in its exports.` + ); + } + + const hasInlineTypes = value.includes('./dist/index.d.ts'); + + // enforce that the correct types directory is present + const present = new Set(pkg.pkgData.files); + if (!present.has('types') && !hasInlineTypes) { + throw new Error( + `Missing types directory from published files for ${pkg.pkgData.name}. This package is using a stable types strategy, and should thus publish a types directory.` + ); + } + if (present.has('unstable-preview-types')) { + throw new Error( + `Unexpected unstable-preview-types directory in published files for ${pkg.pkgData.name}. This package is using a stable types strategy, and should thus publish a types directory.` + ); + } + if (present.has('preview-types')) { + throw new Error( + `Unexpected preview-types directory in published files for ${pkg.pkgData.name}. This package is using a stable types strategy, and should thus publish a types directory.` + ); + } +} + +async function amendFilesForTypesStrategy(pkg: Package, strategy: APPLIED_STRATEGY) { + if (pkg.pkgData.scripts?.['prepack']) { + delete pkg.pkgData.scripts['prepack']; + } + switch (strategy.types) { + case 'private': + await makeTypesPrivate(pkg); + break; + case 'alpha': + await makeTypesAlpha(pkg); + break; + case 'beta': + await makeTypesBeta(pkg); + break; + case 'stable': + await makeTypesStable(pkg); + break; + } + await pkg.file.write(true); +} + +async function restoreTypesStrategyChanges(pkg: Package, _strategy: APPLIED_STRATEGY) { + // restore the package.json to its original state + await exec({ cmd: `git checkout HEAD -- ${pkg.filePath}`, silent: true }); + await pkg.refresh(); + process.stdout.write( + `\t\t♻️ ` + + chalk.grey( + `Successfully Restored Assets Modified for Types Strategy During Publish in ${chalk.cyan(pkg.pkgData.name)}\n` + ) + ); +} diff --git a/release/core/publish/steps/generate-types-tarballs.ts b/release/core/publish/steps/generate-types-tarballs.ts new file mode 100644 index 00000000000..357b2dbcb08 --- /dev/null +++ b/release/core/publish/steps/generate-types-tarballs.ts @@ -0,0 +1,101 @@ +import { PROJECT_ROOT, TARBALL_DIR, toTarballName } from './generate-tarballs'; + +import { exec } from '../../../utils/cmd'; +import path from 'path'; +import fs from 'fs'; +import { APPLIED_STRATEGY, Package } from '../../../utils/package'; + +const INVALID_FILES = new Set([ + 'src', + 'dist', + 'addon', + 'blueprints', + 'dist/docs', + 'addon-test-support', + 'app', + 'index.js', + 'addon-main.js', + 'addon-main.cjs', +]); + +export async function generateTypesTarballs( + config: Map, + packages: Map, + strategy: Map +) { + const tarballDir = path.join(TARBALL_DIR, packages.get('root')!.pkgData.version); + const tmpDir = path.join(PROJECT_ROOT, 'tmp/types', packages.get('root')!.pkgData.version); + fs.mkdirSync(tmpDir, { recursive: true }); + + // for each public package + // if that package has types + // generate a types tarball + // + // to do this we + // copy the types directory to a temporary directory + // create a new package.json + + for (const [, strat] of strategy) { + if (!strat.typesPublish || strat.private || strat.types === 'private') { + continue; + } + + if (strat.types === 'alpha') { + const tmpTypesDir = path.join(tmpDir, strat.typesPublishTo); + fs.mkdirSync(tmpTypesDir, { recursive: true }); + + // create a new package.json + const pkg = packages.get(strat.name)!; + const pkgData = pkg.pkgData; + const newPkgData = { + name: strat.typesPublishTo, + version: pkgData.version, + files: pkgData.files?.filter((f) => !INVALID_FILES.has(f)) ?? [], + private: false, + description: `Type Declarations for ${pkgData.name}`, + author: pkgData.author, + license: pkgData.license, + repository: pkgData.repository, + // try without any peers first + // peerDependencies: pkgData.peerDependencies, + // peerDependenciesMeta: pkgData.peerDependenciesMeta, + }; + const newPkgJson = path.join(tmpTypesDir, 'package.json'); + fs.writeFileSync(newPkgJson, JSON.stringify(newPkgData, null, 2)); + + // // copy the types directory + // const typesDir = path.join(path.dirname(pkg.filePath), 'unstable-preview-types'); + // if (!fs.existsSync(typesDir)) { + // throw new Error(`Types directory does not exist: ${typesDir}`); + // } + // const typesDest = path.join(tmpTypesDir, 'unstable-preview-types'); + // fs.mkdirSync(typesDest, { recursive: true }); + // await exec(`cp -r ${typesDir}/* ${typesDest}`); + + // copy files that are needed + const files = pkgData.files ?? []; + for (const file of files) { + const src = path.join(path.dirname(pkg.filePath), file); + const dest = path.join(tmpTypesDir, file); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + await exec(`cp -r ${src} ${dest}`); + } + + // create the tarball + const tarballName = toTarballName(strat.typesPublishTo); + const tarballPath = path.join(tarballDir, `${tarballName}-${pkg.pkgData.version}.tgz`); + // pack the new package and put it in the tarballs directory + const result = await exec({ + cwd: tmpTypesDir, + cmd: `npm pack --pack-destination=${tarballDir}`, + condense: false, + }); + console.log(result); + + // update the package with the tarball path + pkg.typesTarballPath = tarballPath; + } else { + throw new Error(`Oops! Time to upgrade tis script to handled types strategy: ${strat.types}`); + } + } +} diff --git a/release/core/publish/steps/print-strategy.ts b/release/core/publish/steps/print-strategy.ts new file mode 100644 index 00000000000..d6bb126c84c --- /dev/null +++ b/release/core/publish/steps/print-strategy.ts @@ -0,0 +1,125 @@ +import { TYPE_STRATEGY } from '../../../utils/channel'; +import { getCharLength, getPadding } from '../../../help/-utils'; +import chalk from 'chalk'; +import { AppliedStrategy } from './generate-strategy'; + +export const COLORS_BY_STRATEGY: Record = { + private: 'red', + alpha: 'yellow', + beta: 'cyan', + stable: 'green', +}; + +export function colorName(name: string) { + if (name.startsWith('@warp-drive-types/')) { + return chalk.greenBright('@warp-drive-types/') + chalk.magentaBright(name.substring(18)); + } else if (name.startsWith('@warp-drive-mirror/')) { + return chalk.greenBright('@warp-drive-mirror/') + chalk.magentaBright(name.substring(19)); + } else if (name.startsWith('@warp-drive/')) { + return chalk.greenBright('@warp-drive/') + chalk.magentaBright(name.substring(12)); + } else if (name.startsWith('@ember-data-types/')) { + return chalk.cyanBright('@ember-data-types/') + chalk.yellow(name.substring(18)); + } else if (name.startsWith('@ember-data-mirror/')) { + return chalk.cyanBright('@ember-data-mirror/') + chalk.yellow(name.substring(19)); + } else if (name.startsWith('@ember-data/')) { + return chalk.cyanBright('@ember-data/') + chalk.yellow(name.substring(12)); + } else if (name === 'N/A') { + return chalk.grey(name); + } else { + return chalk.cyan(name); + } +} + +function getPaddedString(str: string, targetWidth: number) { + const width = targetWidth + (str.length - getCharLength(str)); + return str.padEnd(width); +} + +const TABLE_SECTION = Object.freeze([]) as unknown as string[]; + +function printTable(title: string, rows: string[][]) { + const widths = rows[0].map((_, i) => Math.max(...rows.map((row) => getCharLength(row[i])))); + const totalWidth = widths.reduce((acc, width) => acc + width + 3, 1); + const line = getPadding(totalWidth, '-'); + rows.forEach((row, index) => { + if (row === TABLE_SECTION) { + row = rows[index] = []; + widths.forEach((width) => { + row.push(getPadding(width, '-')); + }); + } + }); + const paddedRows = rows.map((row) => row.map((cell, i) => getPaddedString(cell, widths[i]))); + const rowLines = paddedRows.map((row) => `| ${row.join(' | ')} |`); + rowLines.splice(1, 0, line); + const finalRows = `\n\t${chalk.white(chalk.bold(title))}\n\t${line}\n\t` + rowLines.join('\n\t') + `\n\t${line}\n\n`; + + console.log(finalRows); +} + +export async function printStrategy(config: Map, applied: AppliedStrategy) { + const tableRows = [ + [ + ' ', + 'Name', + 'Mirror', + 'Types', + 'From Version', + 'To Version', + 'Stage', + 'Types', + 'NPM Dist Tag', + 'Status', + 'Location', + ], + ]; + applied.public_pks.forEach((applied, name) => { + tableRows.push([ + applied.new ? chalk.magentaBright('New!') : '', + colorName(name), + colorName(applied.mirrorPublishTo), + colorName(applied.typesPublishTo), + chalk.grey(applied.fromVersion), + chalk[COLORS_BY_STRATEGY[applied.stage]](applied.toVersion), + chalk[COLORS_BY_STRATEGY[applied.stage]](applied.stage), + chalk[COLORS_BY_STRATEGY[applied.types]](applied.types), + chalk.magentaBright(applied.distTag), + chalk.cyanBright('public'), + chalk.grey(applied.pkgDir), + ]); + }); + const groups = new Map(); + applied.private_pkgs.forEach((applied, name) => { + let group = groups.get(applied.pkgDir); + if (!group) { + group = []; + groups.set(applied.pkgDir, group); + } + group.push([ + applied.new ? chalk.magentaBright('New!') : '', + colorName(name), + colorName(applied.mirrorPublishTo), + colorName(applied.typesPublishTo), + chalk.grey(applied.fromVersion), + chalk[COLORS_BY_STRATEGY[applied.stage]](applied.toVersion), + chalk[COLORS_BY_STRATEGY[applied.stage]](applied.stage), + chalk[COLORS_BY_STRATEGY[applied.types]](applied.types), + chalk.grey('N/A'), + chalk.yellow('private'), + chalk.grey(applied.pkgDir), + ]); + }); + groups.forEach((group) => { + tableRows.push(TABLE_SECTION); + tableRows.push(...group); + }); + + printTable( + chalk.grey( + `${chalk.white('Release Strategy')} for ${chalk.cyan(config.get('increment'))} bump in ${chalk.cyan( + config.get('channel') + )} channel` + ), + tableRows + ); +} diff --git a/release/core/publish/steps/publish-packages.ts b/release/core/publish/steps/publish-packages.ts new file mode 100644 index 00000000000..35fb51dbb54 --- /dev/null +++ b/release/core/publish/steps/publish-packages.ts @@ -0,0 +1,145 @@ +import chalk from 'chalk'; +import { APPLIED_STRATEGY, Package } from '../../../utils/package'; +import { question } from './confirm-strategy'; +import { exec } from '../../../utils/cmd'; +import { updateDistTag } from '../../promote'; + +export async function publishPackages( + config: Map, + packages: Map, + strategy: Map +) { + const NODE_AUTH_TOKEN = process.env.NODE_AUTH_TOKEN; + const CI = process.env.CI; + let token: string | undefined; + + // allow OTP token usage locally + if (!NODE_AUTH_TOKEN) { + if (CI) { + console.log( + chalk.red( + '🚫 NODE_AUTH_TOKEN not found in ENV. NODE_AUTH_TOKEN is required in ENV to publish from CI. Exiting...' + ) + ); + process.exit(1); + } + token = await getOTPToken(config); + } else { + if (!CI) { + const result = await question( + `\n${chalk.cyan('NODE_AUTH_TOKEN')} found in ENV.\nPublish ${config.get('increment')} release in ${config.get( + 'channel' + )} channel to the ${config.get('tag')} tag on the npm registry? ${chalk.yellow('[y/n]')}:` + ); + const input = result.trim().toLowerCase(); + if (input !== 'y' && input !== 'yes') { + console.log(chalk.red('🚫 Publishing not confirmed. Exiting...')); + process.exit(1); + } + } + } + + let publishCount = 0; + for (const [, strat] of strategy) { + const pkg = packages.get(strat.name)!; + token = await publishPackage(config, strat.distTag, pkg.tarballPath, config.get('dry_run') as boolean, token); + publishCount++; + + if (strat.stage === 'alpha' || strat.stage === 'beta') { + token = await updateDistTag(strat.name, pkg.pkgData.version, 'latest', config.get('dry_run') as boolean, token); + } + + if (strat.mirrorPublish) { + token = await publishPackage( + config, + strat.distTag, + pkg.mirrorTarballPath, + config.get('dry_run') as boolean, + token + ); + publishCount++; + + if (strat.stage === 'alpha' || strat.stage === 'beta') { + token = await updateDistTag( + strat.mirrorPublishTo, + pkg.pkgData.version, + 'latest', + config.get('dry_run') as boolean, + token + ); + } + } + if (strat.typesPublish) { + token = await publishPackage( + config, + strat.distTag, + pkg.typesTarballPath, + config.get('dry_run') as boolean, + token + ); + publishCount++; + + if (strat.stage === 'alpha' || strat.stage === 'beta') { + token = await updateDistTag( + strat.typesPublishTo, + pkg.pkgData.version, + 'latest', + config.get('dry_run') as boolean, + token + ); + } + } + } + + console.log(`✅ ` + chalk.cyan(`published ${chalk.greenBright(publishCount)} 📦 packages to npm`)); +} + +export async function getOTPToken(config: Map, reprompt?: boolean) { + const prompt = reprompt + ? `The provided OTP token has expired. Please enter a new OTP token: ` + : `\nℹ️ ${chalk.cyan( + 'NODE_AUTH_TOKEN' + )} not found in ENV.\n\nConfiguring NODE_AUTH_TOKEN is the preferred mechanism by which to publish. Alternatively you may continue using an OTP token.\n\nPublishing ${config.get( + 'increment' + )} release in ${config.get('channel')} channel to the ${config.get( + 'tag' + )} tag on the npm registry.\n\nEnter your OTP token: `; + + let token = await question(prompt); + + return token.trim(); +} + +async function publishPackage( + config: Map, + distTag: string, + tarball: string, + dryRun: boolean, + otp?: string +): Promise { + let cmd = `npm publish ${tarball} --tag=${distTag} --access=public`; + + if (otp) { + cmd += ` --otp=${otp}`; + } + + if (dryRun) { + cmd += ' --dry-run'; + } + + try { + await exec({ cmd, condense: true }); + } catch (e) { + if (!otp || !(e instanceof Error)) { + throw e; + } + if (e.message.includes('E401') || e.message.includes('EOTP')) { + otp = await getOTPToken(config, true); + return publishPackage(config, distTag, tarball, dryRun, otp); + } else { + throw e; + } + } + + return otp; +} diff --git a/release/core/release-notes/index.ts b/release/core/release-notes/index.ts new file mode 100644 index 00000000000..a745aea5d3b --- /dev/null +++ b/release/core/release-notes/index.ts @@ -0,0 +1,57 @@ +import { parseRawFlags } from '../../utils/parse-args'; +import { printHelpDocs } from '../../help/docs'; +import { GIT_TAG, getAllPackagesForGitTag, getGitState } from '../../utils/git'; +import { gatherPackages, loadStrategy } from '../../utils/package'; +import { applyStrategy } from '../publish/steps/generate-strategy'; +import { printStrategy } from '../publish/steps/print-strategy'; +import { confirmStrategy } from '../publish/steps/confirm-strategy'; +import { release_notes_flags_config } from '../../utils/flags-config'; +import { SEMVER_VERSION } from '../../utils/channel'; +import { updateChangelogs } from './steps/update-changelogs'; +import { getChanges } from './steps/get-changes'; +import { confirmCommitChangelogs } from './steps/confirm-changelogs'; + +export async function executeReleaseNoteGeneration(args: string[]) { + // remove the command itself from the list + args.shift(); + + // get user supplied config + const config = await parseRawFlags(args, release_notes_flags_config); + + if (config.full.get('help')) { + return printHelpDocs(args); + } + + // get git info + await getGitState(config.full); + + // get configured strategy + const strategy = await loadStrategy(); + + // get packages present in the git tag version + const fromVersion = config.full.get('from') as SEMVER_VERSION; + const fromTag = `v${fromVersion}` as GIT_TAG; + const baseVersionPackages = await getAllPackagesForGitTag(fromTag); + + // get packages present on our current branch + const packages = await gatherPackages(strategy.config); + + // get applied strategy + const applied = await applyStrategy(config.full, strategy, baseVersionPackages, packages); + + // print strategy to be applied + await printStrategy(config.full, applied); + + // confirm we should continue + await confirmStrategy(); + + // generate the list of changes + const newChanges = await getChanges(strategy, packages, fromTag); + + // update all changelogs, including the primary changelog + // and the changelogs for each package in changelogRoots + // this will not commit the changes + const changedFiles = await updateChangelogs(fromTag, newChanges, config.full, strategy, packages, applied); + + await confirmCommitChangelogs(changedFiles, config.full, applied); +} diff --git a/release/core/release-notes/steps/confirm-changelogs.ts b/release/core/release-notes/steps/confirm-changelogs.ts new file mode 100644 index 00000000000..d806a35e855 --- /dev/null +++ b/release/core/release-notes/steps/confirm-changelogs.ts @@ -0,0 +1,42 @@ +import { BunFile } from 'bun'; +import { confirm } from '../../publish/steps/confirm-strategy'; +import { exec } from '../../../utils/cmd'; +import chalk from 'chalk'; +import { AppliedStrategy } from '../../publish/steps/generate-strategy'; + +export async function confirmCommitChangelogs( + _changedFiles: BunFile[], + config: Map, + strategy: AppliedStrategy +) { + const dryRun = config.get('dry_run') as boolean; + + if (config.get('commit') === false) { + console.log(chalk.grey(`\t➠ Skipped commit of changelogs.`)); + return; + } + + try { + await confirm({ + prompt: `Do you want to commit the changelogs?`, + cancelled: `🚫 Commit of changelogs cancelled. Exiting...`, + }); + } finally { + if (dryRun) { + // cleanup files because we're not actually committing + await exec(['sh', '-c', `git add -A && git reset --hard HEAD`]); + } + } + + if (!dryRun) { + const newVersion = strategy.all.get('root')!.toVersion; + await exec(['sh', '-c', `git add -A && git commit -m "chore: update changelogs for v${newVersion}"`]); + + if (config.get('upstream')) { + await exec(['sh', '-c', `git push`]); + console.log(chalk.grey(`\t✅ pushed changelog commit to upstream.`)); + } else { + console.log(chalk.grey(`\t➠ Skipped push of changelogs.`)); + } + } +} diff --git a/release/core/release-notes/steps/get-changes.ts b/release/core/release-notes/steps/get-changes.ts new file mode 100644 index 00000000000..6336452235b --- /dev/null +++ b/release/core/release-notes/steps/get-changes.ts @@ -0,0 +1,189 @@ +import { exec } from '../../../utils/cmd'; +import { Package, STRATEGY } from '../../../utils/package'; +import path from 'path'; + +export const Committers = Symbol('Committers'); +export type Entry = { packages: string[]; description: string; committer: string }; +export interface LernaOutput { + [Committers]: Map; + [key: string]: Map; +} +export type LernaChangeset = { + data: LernaOutput; + byPackage: Record>>; +}; + +const IgnoredPackages = new Set(['private-build-infra']); + +// e.g. match lines ending in "asljasdfjh ([@runspired](https://github.com/runspired))"" +const CommitterRegEx = /.*\s\(?\[@([a-zA-Z0-9-]+)\]\(https:\/\/github.com\/\1\)\)?$/; + +function keyForLabel(label: string, strategy: STRATEGY): string { + const labelKey = strategy.config.changelog?.collapseLabels?.labels.some((v) => v === label); + return labelKey ? strategy.config.changelog!.collapseLabels!.title : label; +} + +function packagesBySubPath(strategy: STRATEGY, packages: Map): Map { + const subPathMap = new Map(); + const changelogRoots = strategy.config.changelogRoots || strategy.config.packageRoots; + const changelogPaths = changelogRoots.map((v) => v.replace('/*', '')); + + for (const [, pkg] of packages) { + if (pkg.pkgData.name === 'root') { + subPathMap.set('root', pkg); + continue; + } + let relative = path.dirname(path.relative(process.cwd(), pkg.filePath)); + for (const root of changelogPaths) { + if (relative.startsWith(root + '/')) { + const shortPath = relative.substring(root.length + 1); + if (subPathMap.has(shortPath)) { + console.error(`Duplicate subpath: ${shortPath}`); + process.exit(1); + } + relative = shortPath; + break; + } + } + subPathMap.set(relative, pkg); + } + + const mappings = strategy.config.changelog?.mappings || {}; + Object.keys(mappings).forEach((mapping) => { + const mapped = mappings[mapping]; + if (mapped === null) { + subPathMap.set(mapping, packages.get('root')!); + return; + } + const pkg = packages.get(mapped); + if (!pkg) { + throw new Error(`Could not find package for mapping: ${mapping}`); + } + subPathMap.set(mapping, pkg); + }); + + return subPathMap; +} + +function packageForSubPath(strategy: STRATEGY, subPath: string, packages: Map): string | null { + if (IgnoredPackages.has(subPath)) { + return null; + } + const pkg = packages.get(subPath); + if (pkg) { + return pkg.pkgData.name; + } + throw new Error(`Could not find package for subpath: ${subPath}`); +} + +function extractLoggedEntry( + currentEntry: Entry, + data: LernaOutput, + byPackage: Record>>, + subPathMap: Map, + strategy: STRATEGY, + currentSection: string +): void { + const PRMatches = currentEntry!.description.match(/^\[#(\d+)/); + const PRNumber = PRMatches![1]; + + // e.g. ([@runspired](https://github.com/runspired)) + const committerMatches = currentEntry!.description.match(CommitterRegEx); + currentEntry!.committer = committerMatches![1]; + + (data[currentSection] as Map).set(PRNumber, currentEntry as Entry); + + currentEntry?.packages.forEach((subPath) => { + const pkg = packageForSubPath(strategy, subPath, subPathMap); + + if (pkg) { + byPackage[pkg] = byPackage[pkg] || {}; + byPackage[pkg][currentSection] = byPackage[pkg][currentSection] || new Map(); + byPackage[pkg][currentSection].set(PRNumber, currentEntry as Entry); + } + }); +} + +function parseLernaOutput(markdown: string, strategy: STRATEGY, packages: Map): LernaChangeset { + // uncomment this to see lerna's markdown output if needed to debug + // console.log(markdown); + const subPathMap = packagesBySubPath(strategy, packages); + const data: LernaOutput = { + [Committers]: new Map(), + }; + const byPackage: Record>> = {}; + const lines = markdown.split('\n'); + + let isParsingCommitters = false; + let isParsingSection = false; + let currentSection = ''; + let currentEntry: Entry | null = null; + // console.log('lines', lines); + + for (const line of lines) { + if (isParsingSection) { + if (line === '') { + isParsingSection = false; + currentSection = ''; + } else { + if (line.startsWith('* [#')) { + currentEntry = { + packages: ['Other'], + description: line.substring(2), + committer: '', + }; + extractLoggedEntry(currentEntry, data, byPackage, subPathMap, strategy, currentSection); + } else if (line.startsWith('* ')) { + const packages = line + .substring(2) + .split(',') + .map((v) => v.trim().replaceAll('`', '')); + currentEntry = { + packages, + description: '', + committer: '', + }; + } else if (line.startsWith(' * ')) { + currentEntry = structuredClone(currentEntry!); + currentEntry!.description = line.substring(4); + extractLoggedEntry(currentEntry, data, byPackage, subPathMap, strategy, currentSection); + } else { + isParsingSection = false; + currentSection = ''; + currentEntry = null; + } + } + } else if (isParsingCommitters) { + if (line === '') { + isParsingCommitters = false; + } else { + const committerMatches = line.match(CommitterRegEx); + const committer = committerMatches![1]; + data[Committers].set(committer, line.substring(2)); + } + } else if (line.startsWith('#### ')) { + isParsingCommitters = false; + isParsingSection = false; + currentEntry = null; + if (line.startsWith('#### Committers:')) { + currentSection = 'Committers'; + isParsingCommitters = true; + } else { + currentSection = keyForLabel(line.substring(5), strategy); + data[currentSection] = data[currentSection] || new Map(); + isParsingSection = true; + } + } + } + + // Object.entries(data).forEach(([key, value]) => { + // console.log(key, value); + // }); + + return { data, byPackage }; +} + +export async function getChanges(strategy: STRATEGY, packages: Map, fromTag: string) { + const changelogMarkdown = await exec(['sh', '-c', `bunx lerna-changelog --from=${fromTag}`]); + return parseLernaOutput(changelogMarkdown, strategy, packages); +} diff --git a/packages/unpublished-test-infra/tests/dummy/app/styles/app.css b/release/core/release-notes/steps/submit-pr-to-branch.ts similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/app/styles/app.css rename to release/core/release-notes/steps/submit-pr-to-branch.ts diff --git a/release/core/release-notes/steps/update-changelogs.ts b/release/core/release-notes/steps/update-changelogs.ts new file mode 100644 index 00000000000..0d72d2ebe89 --- /dev/null +++ b/release/core/release-notes/steps/update-changelogs.ts @@ -0,0 +1,132 @@ +import { Package, STRATEGY } from '../../../utils/package'; +import { AppliedStrategy } from '../../publish/steps/generate-strategy'; +import { Committers, Entry, LernaChangeset } from './get-changes'; +import path from 'path'; +import chalk from 'chalk'; +import { BunFile } from 'bun'; + +function findInsertionPoint(lines: string[], version: string) { + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith(`## ${version}`)) { + return i; + } + } + return 2; +} + +function buildText( + newTag: string, + strategy: STRATEGY, + changes: Record>, + committerStrings: Map +): string[] { + // YYYY-MM-DD + const formattedDate = new Date().toISOString().split('T')[0]; + const committers = new Set(); + + const lines = [`## ${newTag} (${formattedDate})`, '']; + const order = strategy.config.changelog?.labelOrder || []; + const seen = new Set(); + + for (const section of order) { + const entries = changes[section]; + if (!entries) { + continue; + } + + lines.push(`#### ${section}`, ''); + for (const [pr, entry] of entries) { + committers.add(entry.committer); + lines.push(`* ${entry.description}`); + } + lines.push(''); + seen.add(section); + } + + for (const [section, entries] of Object.entries(changes)) { + if (section === 'Committers' || seen.has(section)) { + continue; + } + + lines.push(`#### ${section}`, ''); + for (const [pr, entry] of entries) { + committers.add(entry.committer); + lines.push(`* ${entry.description}`); + } + lines.push(''); + } + + lines.push(`#### Committers: (${committers.size})`, ''); + committers.forEach((committer) => { + // e.g. `* [@runspired](https://github.com/runspired)` + lines.push(committerStrings.get(committer)!); + }); + lines.push(''); + + return lines; +} + +export async function updateChangelogs( + fromTag: string, + newChanges: LernaChangeset, + config: Map, + strategy: STRATEGY, + packages: Map, + applied: AppliedStrategy +): Promise { + const file = Bun.file('./CHANGELOG.md'); + const mainChangelog = await file.text(); + const lines = mainChangelog.split('\n'); + const toVersion = applied.all.get('root')!.toVersion; + const toTag = `v${toVersion}`; + const newLines = buildText(toTag, strategy, newChanges.data, newChanges.data[Committers]); + const insertionPoint = findInsertionPoint(lines, fromTag); + lines.splice(insertionPoint, 0, ...newLines); + await Bun.write(file, lines.join('\n')); + console.log(`\t✅ Updated Primary Changelog`); + const changedFiles = [file]; + + for (const [pkgName, changes] of Object.entries(newChanges.byPackage)) { + if (pkgName === 'root') { + continue; + } + + const pkg = packages.get(pkgName); + if (!pkg) { + throw new Error(`Could not find package for name: ${pkgName}`); + } + const changelogFile = Bun.file(path.join(path.dirname(pkg.filePath), 'CHANGELOG.md')); + const exists = await changelogFile.exists(); + const toVersion = applied.all.get(pkgName)!.toVersion; + const toTag = `v${toVersion}`; + const fromVersion = applied.all.get(pkgName)!.fromVersion; + const fromTag = `v${fromVersion}`; + const newLines = buildText(toTag, strategy, changes, newChanges.data[Committers]); + changedFiles.push(changelogFile); + + let changelogLines: string[] = []; + if (!exists) { + changelogLines = [ + `# ${pkg.pkgData.name} Changelog`, + '', + `For the full project changelog see [https://github.com/emberjs/data/blob/main/CHANGELOG.md](https://github.com/emberjs/data/blob/main/CHANGELOG.md)`, + '', + ...newLines, + '', + ]; + } else { + changelogLines = (await changelogFile.text()).split('\n'); + const insertionPoint = findInsertionPoint(changelogLines, fromTag); + changelogLines.splice(insertionPoint, 0, ...newLines); + } + + await Bun.write(changelogFile, changelogLines.join('\n')); + console.log( + exists + ? `\t✅ Updated ${chalk.cyan(pkg.pkgData.name)} Changelog` + : `\t✅ Created ${chalk.cyan(pkg.pkgData.name)} Changelog` + ); + } + + return changedFiles; +} diff --git a/release/core/utils/next-version.ts b/release/core/utils/next-version.ts new file mode 100644 index 00000000000..ce9fbd93c9e --- /dev/null +++ b/release/core/utils/next-version.ts @@ -0,0 +1,155 @@ +import { STRATEGY_TYPE, SEMVER_VERSION, CHANNEL } from '../../utils/channel'; + +import semver from 'semver'; + +/** + * "Next Version" is a complicated subject. + * + * Disregarding "strategy" for the moment: + * + * If we are beta channel or canary channel + * - then next patch means next prerelease e.g. from 1.0.0-beta.1 => 1.0.0-beta.2 + * - next minor means 1.0.0-beta.3 => 1.1.0-beta.1 + * - next major means 1.4.0-beta.3 => 2.0.0-beta.1 + * + * If we are a release channel + * - then next patch means next patch e.g. from 1.0.0 => 1.0.1 + * - next minor means 1.0.1 => 1.1.0 (valid only in a "re-release") + * - next major means 1.1.0 => 2.0.0 (valid only in a "re-release") + * + * If we are any other channel, then only next patch is allowed. + * + * To promote an alpha to beta and a beta to release: + * + * 1.0.0-alpha.3 => 1.0.0-beta.1 is a "patch" performed via beta channel + * 1.0.0-beta.3 => 1.0.0 is a "patch" performed via release channel + * + * However, "strategy" amends these rules. When considering strategy, the + * above description applies to a "stable" package. + * + * ## Beta strategy adjustments + * + * If our strategy is "beta" then our "major" version should be "0". + * + * If we are a beta channel or canary channel + * - then next patch means next prerelease e.g. from 0.1.0-beta.1 => 0.1.0-beta.2 + * - next minor increments the third number e.g. from 0.1.0-beta.2 => 0.1.1-beta.1 + * - next major means to bump the second number e.g. from 0.1.1-beta.3 => 0.2.0-beta.1 + * + * If we are a release channel + * - then next patch means next patch e.g. from 0.1.0 => 0.1.1 + * - next minor is equivalent to next patch e.g. 0.1.1 => 0.1.2 (valid only in a "re-release") + * - next major increments the second number instead e.g. 0.1.1 => 0.2.0 (valid only in a "re-release") + * + * ## Alpha strategy adjustments + * + * If our strategy is "alpha" then our "major" version and our "minor" version should be "0". + * + * If we are a beta channel or canary channel + * - then next patch means next prerelease e.g. from 0.0.0-beta.1 => 0.0.0-beta.2 + * - next minor increments the prerelease as well e.g. from 0.0.0-beta.1 => 0.0.0-beta.2 + * - next major means to bump the third number e.g. from 0.0.1-beta.3 => 0.0.2-beta.1 + * + * If we are a release channel + * - then next major, minor or patch all increment the third number e.g. from 0.0.0 => 0.0.1 + * + * + * + * For re-release we will need to graph all versions associated with the prior release + * and increment those somehow as fromVersion. + */ + +export function getNextMajor(fromVersion: SEMVER_VERSION, channel: CHANNEL, strategy: STRATEGY_TYPE): SEMVER_VERSION { + if (channel !== 'canary' && channel !== 'beta' && channel !== 'release') { + throw new Error(`You cannot increment the major version directly within the '${channel}' channel.`); + } + + switch (strategy) { + case 'alpha': + if (channel === 'canary') return semver.inc(fromVersion, 'prepatch', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prepatch', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'patch') as SEMVER_VERSION; + + case 'beta': + if (channel === 'canary') return semver.inc(fromVersion, 'preminor', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'preminor', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'minor') as SEMVER_VERSION; + + case 'stable': + if (channel === 'canary') return semver.inc(fromVersion, 'premajor', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'premajor', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'major') as SEMVER_VERSION; + + default: + throw new Error(`Unexpected strategy '${strategy}'`); + } +} + +export function getNextMinor(fromVersion: SEMVER_VERSION, channel: CHANNEL, strategy: STRATEGY_TYPE): SEMVER_VERSION { + if (channel !== 'canary' && channel !== 'beta' && channel !== 'release') { + throw new Error(`You cannot increment the minor version directly within the '${channel}' channel.`); + } + + switch (strategy) { + case 'alpha': + if (channel === 'canary') return semver.inc(fromVersion, 'prerelease', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prerelease', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'patch') as SEMVER_VERSION; + + case 'beta': + if (channel === 'canary') return semver.inc(fromVersion, 'prepatch', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prepatch', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'minor') as SEMVER_VERSION; + + case 'stable': + if (channel === 'canary') return semver.inc(fromVersion, 'preminor', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'preminor', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'minor') as SEMVER_VERSION; + + default: + throw new Error(`Unexpected strategy '${strategy}'`); + } +} + +export function getNextPatch(fromVersion: SEMVER_VERSION, channel: CHANNEL, strategy: STRATEGY_TYPE): SEMVER_VERSION { + switch (strategy) { + case 'alpha': + if (channel === 'canary') return semver.inc(fromVersion, 'prerelease', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prerelease', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'patch') as SEMVER_VERSION; + + case 'beta': + if (channel === 'canary') return semver.inc(fromVersion, 'prerelease', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prerelease', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'patch') as SEMVER_VERSION; + + case 'stable': + if (channel === 'canary') return semver.inc(fromVersion, 'prerelease', 'alpha') as SEMVER_VERSION; + if (channel === 'beta') return semver.inc(fromVersion, 'prerelease', 'beta') as SEMVER_VERSION; + return semver.inc(fromVersion, 'patch') as SEMVER_VERSION; + + default: + throw new Error(`Unexpected strategy '${strategy}'`); + } +} + +export function getNextVersion( + fromVersion: SEMVER_VERSION, + channel: CHANNEL, + increment: 'major' | 'minor' | 'patch', + strategy: STRATEGY_TYPE +): SEMVER_VERSION { + switch (increment) { + case 'major': { + return getNextMajor(fromVersion, channel, strategy); + } + case 'minor': { + return getNextMinor(fromVersion, channel, strategy); + } + case 'patch': { + return getNextPatch(fromVersion, channel, strategy); + } + default: + throw new Error(`Unexpected version increment method '${increment}'`); + } +} diff --git a/release/guide/canary.md b/release/guide/canary.md new file mode 100644 index 00000000000..27270d31dae --- /dev/null +++ b/release/guide/canary.md @@ -0,0 +1,26 @@ +# Canary Release Guide + +## Automated Workflow + +The [Release > Canary](../../.github/workflows/release_publish-canary.yml) workflow should be used to publish all new canaries from the [Action Overview](https://github.com/emberjs/data/actions/workflows/release_publish-canary.yml). + +This workflow trigger is restricted to project maintainers. + +For the first release of a new cycle, manually running this flow with the increment as either `major` or `minor` is required. + +Subsequent pre-release versions will be auto-released on a chron schedule. + + +## Manually Canarying + +Ensure you have bun, node and pnpm configured correctly. Volta is preferred for managing +node and pnpm versions. For bun, any `1.x` version should work but minimum version should +ideally match the installed `bun-types` dependency `package.json`. + +We always release canary from the `main` branch, though forcing from another branch is possible if required in a last resort. + +```ts +bun release publish canary -i +``` + +Run `bun release help` for additional options. diff --git a/release/help/-utils.ts b/release/help/-utils.ts new file mode 100644 index 00000000000..e62ec6fa74c --- /dev/null +++ b/release/help/-utils.ts @@ -0,0 +1,201 @@ +import chalk from 'chalk'; + +export function getCharLength(str: string | undefined): number { + if (!str) { + return 0; + } + // The string iterator that is used here iterates over characters, + // not mere code units + let count = 0; + let isColorChar = false; + + // color chars follow the pattern + // \u001b[m + for (const char of str) { + if (isColorChar && char === 'm') { + isColorChar = false; + } else if (char === '\u001b' || char === '\u001B') { + isColorChar = true; + } else if (!isColorChar) { + count++; + } + } + + return count; +} + +export function adjustForWords(str: string, max_length: number) { + // The string iterator that is used here iterates over characters, + // not mere code units + let count = 0; + let len = 0; + let lastWhitespaceLen = 0; + let isColorChar = false; + let isMaybeStartingWhitespace = true; + + // color chars follow the pattern + // \u001b[m + for (const char of str) { + let charIndex = len; + len++; + if (isMaybeStartingWhitespace) { + if (char === ' ' || char === '\t') { + // increment len but not count + lastWhitespaceLen = charIndex; + continue; + } else { + isMaybeStartingWhitespace = false; + } + } + + if (isColorChar && char === 'm') { + isColorChar = false; + } else if (char === '\u001b') { + isColorChar = true; + } else if (!isColorChar) { + count++; + if (count > max_length) { + return lastWhitespaceLen; + } + if (char === ' ' || char === '\t') { + lastWhitespaceLen = charIndex; + } + } + } + + return len; +} + +export function indent(str: string, depth = 1) { + const indentStr = getPadding(depth); + return str + .split('\n') + .map((line) => { + return indentStr + line; + }) + .join('\n'); +} + +export function getPadding(depth: number, filler = '\t') { + return new Array(depth).fill(filler).join(''); +} + +export function getNumTabs(str: string) { + let len = Math.max(4, str.length); + len = Math.min(len, 8); + return 3 - Math.round(len / 4); +} + +/** + * colorizes a string based on color<<>> syntax + * where color is one of the following: + * - gr (grey) + * - bg (brightGreen) + * - bm (brightMagenta) + * - cy (cyan) + * - ye (yellow) + * + * e.g. + * + * color`This is gr<> and this is bg<> and this is bm<> and this is cy<> and this is ye<>` + */ +export function color(str: string) { + const colors = { + gr: 'grey', + gb: 'greenBright', + mb: 'magentaBright', + cy: 'cyan', + ye: 'yellow', + }; + + const colorized = str.replace(/(\w+)<<(.+?)>>/g, (match, color, text) => { + const c = colors[color]; + if (!c) { + throw new Error(`Unknown color ${color}`); + } + return chalk[c](text); + }); + + return colorized; +} + +// TODO if the last line of a context is too long we don't always +// end up rebalancing correctly. We need to splice an additional +// line in in this case. If we do this on a rolling basis its +// probably easier. +export function rebalanceLines(str: string, max_length = 75): string { + const lines = str.split('\n'); + let inContext = false; + let contextIndent = ''; + let contextHasBullet = false; + let contextIndex = 0; + let contextBulletIndent = ''; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line === '') { + inContext = false; + continue; + } + if (line.trim() === '---') { + lines[i] = chalk.grey(getPadding(max_length, '-')); + inContext = false; + continue; + } + if (line.trim() === '===') { + lines[i] = chalk.grey(getPadding(max_length, '=')); + inContext = false; + continue; + } + + let indentMatch = line.match(/^\s+/); + let hasBullet = line.match(/^\s*[-\*]\s+/) || line.match(/^\s*\d+\)?\.\s+/); + let bulletIndent = hasBullet ? hasBullet[0] : ''; + let indent = ''; + + if (indentMatch) { + indent = indentMatch[0]; + } + + // if we have our own bullet, this is a new context + // so nothing can be rebalanced + if (hasBullet || !inContext) { + contextIndex = i; + inContext = true; + contextIndent = indent; + contextHasBullet = Boolean(hasBullet); + contextBulletIndent = bulletIndent; + continue; + } + + const isBulletContinuation = + contextHasBullet && indent.startsWith(contextIndent) && indent.length === contextBulletIndent.length; + const isNonBulletContinuation = !contextHasBullet && indent === contextIndent; + + // determine if we match + if (isBulletContinuation || isNonBulletContinuation) { + // we are in the same context + // rebalance if needed + const fullText = lines[contextIndex] + ' ' + line.slice(indent.length); + const len = adjustForWords(fullText, max_length); + const prevLine = fullText.slice(0, len).trimEnd(); + const thisLine = indent + fullText.slice(len).trim(); + lines[contextIndex] = prevLine; + lines[i] = thisLine || (null as unknown as string); + if (thisLine) { + // only update if we have content on the line + contextIndex = i; + } + } else { + // we are a new context + contextIndex = i; + inContext = true; + contextIndent = (indent as unknown as string) || ''; + contextHasBullet = false; + contextBulletIndent = ''; + continue; + } + } + + return lines.filter((l) => l !== null).join('\n'); +} diff --git a/release/help/docs.ts b/release/help/docs.ts new file mode 100644 index 00000000000..f567cdb0388 --- /dev/null +++ b/release/help/docs.ts @@ -0,0 +1,96 @@ +import chalk from 'chalk'; +import { command_config } from '../utils/flags-config'; +import { Command, Flag } from '../utils/parse-args'; +import { color, getNumTabs, getPadding, indent } from './-utils'; + +function getDefaultValueDescriptor(value: unknown) { + if (typeof value === 'string') { + return chalk.green(`"${value}"`); + } else if (typeof value === 'number') { + return chalk.green(`${value}`); + } else if (typeof value === 'boolean') { + return chalk.green(`${value}`); + } else if (value === null) { + return chalk.green('null'); + } else if (typeof value === 'function') { + if (value.name) { + return chalk.cyan(`Function<${value.name}>`); + } else { + return chalk.cyan(`Function`); + } + } else { + return chalk.grey('N/A'); + } +} + +function buildOptionDoc(flag: Flag, index: number): string { + const { flag_aliases, flag_mispellings, description, examples } = flag; + const flag_shape = + chalk.magentaBright(flag.positional ? `<${flag.flag}>` : `--${flag.flag}`) + + (flag.required ? chalk.yellow(chalk.italic(` required`)) : ''); + const flag_aliases_str = chalk.grey(flag_aliases?.join(', ') || 'N/A'); + const flag_mispellings_str = chalk.grey(flag_mispellings?.join(', ') || 'N/A'); + + return `${flag_shape} ${chalk.greenBright(flag.name)} + ${indent(description, 1)} + ${chalk.yellow('default')}: ${getDefaultValueDescriptor(flag.default_value)} + ${chalk.yellow('aliases')}: ${flag_aliases_str} + ${chalk.yellow('alt')}: ${flag_mispellings_str} + ${chalk.grey('Examples')}: + ${examples + .map((example) => { + if (typeof example === 'string') { + return example; + } else { + return `${example.desc}\n\t\t${example.example.join('\n\t\t')}`; + } + }) + .join('\n\t')}`; +} + +function buildCommandDoc(command: Command, index: number): string { + const { name, cmd, description, alt, options, overview, example } = command; + let xmpl: string | undefined = ''; + + if (Array.isArray(example)) { + xmpl = example.join('\n\t '); + } else { + xmpl = example; + } + + const lines = [ + `cy<<${chalk.bold(cmd)}>>\n${indent(description, 1)}`, + alt ? `\tye<>: gr<<${alt.join(', ')}>>` : '', + overview ? `\t${overview}` : '', + xmpl ? `\n\tgr<<${Array.isArray(example) ? 'Examples' : 'Example'}>>:` : '', + xmpl ? `\t ${xmpl}\n` : '', + ].filter(Boolean); + + const opts = options ? Object.values(options) : []; + if (opts.length > 0) { + lines.push( + `\t${chalk.bold(chalk.yellowBright('Options'))}`, + indent(`${Object.values(opts).map(buildOptionDoc).join('\n\n')}`, 1) + ); + } + + return color(lines.join('\n')); +} + +export async function printHelpDocs(_args: string[]) { + const commands = Object.values(command_config); + + console.log( + indent( + `${chalk.bold('Usage')} +$ ./publish/index.ts ${chalk.magentaBright('')} [options] + + + +${chalk.bold('Commands')} + ${commands.map(buildCommandDoc).join('\n ')} + +` + ) + ); +} diff --git a/release/help/sections/about.ts b/release/help/sections/about.ts new file mode 100644 index 00000000000..ebc472b1856 --- /dev/null +++ b/release/help/sections/about.ts @@ -0,0 +1,92 @@ +import { color, indent, rebalanceLines } from '../-utils'; + +export const ABOUT = new Set([ + 'about', + 'abt', + 'abut', + 'aboot', + 'abt', + 'describe', + 'desc', + 'dsc', + 'dscr', + 'dscrb', + 'why', + 'y', + 'a', + 'd', +]); + +export const About = `ye<<#>> About + +This script is used to automate the release process for cy<>. + +=== + +ye<<##>> Who Can Release? + +It is intended that this process is run in CI (ENV cy<>); +however, it is able to be run manually as well. + +For both CI and locally it is expected that ENV contains a cy<> +with proper permissions to publish the various packages within the ember-data NPM +organization. + +Users (or CI) will also need special permission to push to main, beta, lts, lts-*-* +and release-*-* branches. For CI based triggers, the user triggering MUST have been +given permission to trigger the workflow. + +--- + +ye<<##>> Process Overview + +The release process is carried out in a multi-step process, where +each step is capable of being run independently. This allows for +a release to be potentially recoverable in the event of a failure +in one step when prior steps have succeeded. + +Releases are governed by a mb<>. The strategy is determined by +the mb<> being released to, the intended version change of the release +(ye<<'major'>>, ye<<'minor'>> or ye<<'patch'>>), and the cy<<./strategy.json>> +file in the publish directory. + +Each package follows both a general release strategy based on its maturity +AND a typescript release strategy based on the maturity of its type definitions. + +\tye<<###>> Steps + +\t[All] +\t1. ensure local branch is clean and sync'd to upstream +\t2. generate release plan +\t - if not CI, confirm release plan + +\t[All but beta/canary] +\t3. generate changelog PR against current branch +\t - if not CI, leave open change against current branch +\t4. confirm changelog PR merged to current branch +\t - if not CI, confirm changelog committed and upstream updated +\t5. PR changelog updates to main branch +\t - if not CI, output warning to do this manually + +\t[All] +\t6. bump versions & update referenced versions throughout the project +\t - if CI, this is triggered automatically by the merge of the changelog PR +\t7. commit and tag version changes, push to upstream +\t8. inter-package dependency and peer-dependency ranges are patched according +\t to the strategy defined, this is not committed +\t9. packages are patched to remove or rename files according to the typescript +\t strategy defined, this is not committed. +\t10. prepackage tarballs +\t11. publish tarballs to npm +\t12. reset local state + +\t[All but beta/canary] +\t13. generate a github Release with notes + +--- + +`; + +export function printAbout(args: string[]) { + console.log(indent(rebalanceLines(color(About)), 1)); +} diff --git a/release/help/sections/manual.ts b/release/help/sections/manual.ts new file mode 100644 index 00000000000..f9aa145fb67 --- /dev/null +++ b/release/help/sections/manual.ts @@ -0,0 +1,15 @@ +export const HELP = new Set([ + 'doc', + 'docs', + 'guide', + 'h', + 'halp', + 'he', + 'hel', + 'help', + 'hlp', + 'm', + 'man', + 'mn', + 'usage', +]); diff --git a/release/index.ts b/release/index.ts new file mode 100755 index 00000000000..555ea400ea5 --- /dev/null +++ b/release/index.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env bun +import chalk from 'chalk'; +import { printHelpDocs } from './help/docs'; +import { normalizeFlag } from './utils/parse-args'; +import { getCommands } from './utils/flags-config'; +import { printAbout } from './help/sections/about'; +import { executePublish } from './core/publish'; +import { executeReleaseNoteGeneration } from './core/release-notes'; +import { write } from './utils/write'; +import { promoteToLTS } from './core/promote'; +import { latestFor } from './core/latest-for'; + +const COMMANDS = { + help: printHelpDocs, + about: printAbout, + release_notes: executeReleaseNoteGeneration, + publish: executePublish, + latest_for: latestFor, + promote: promoteToLTS, + default: executePublish, + exec: async (args: string[]) => { + const cmd = args.shift(); + + if (!cmd) { + throw new Error('No command provided to exec'); + } + + const commands = getCommands(); + const cmdString = (commands.get(normalizeFlag(cmd)) as keyof typeof COMMANDS) || 'default'; + + const command = COMMANDS[cmdString]; + if (command) { + await command( + args.filter((arg) => { + return !arg.endsWith('='); + }) + ); + } else { + throw new Error(`Command not found: ${cmd}`); + } + }, +}; + +async function main() { + const args = Bun.argv.slice(2); + + const commandArg = args.length === 0 ? 'help' : normalizeFlag(args[0]); + const commands = getCommands(); + const cmdString = (commands.get(commandArg) as keyof typeof COMMANDS) || 'default'; + const cmd = COMMANDS[cmdString]; + + // we silence output for the latest_for command + if (cmdString !== 'latest_for') { + write( + chalk.grey( + `\n\t${chalk.bold( + chalk.greenBright('Warp') + chalk.magentaBright('Drive') + )} | Automated Release\n\t==============================` + ) + chalk.grey(`\n\tengine: ${chalk.cyan('bun@' + Bun.version)}\n`) + ); + } + + if (args.length && commands.has(commandArg)) { + args.shift(); + } + + await cmd(args); + process.exit(0); +} + +await main(); diff --git a/release/strategy.json b/release/strategy.json new file mode 100644 index 00000000000..2146afbc7d2 --- /dev/null +++ b/release/strategy.json @@ -0,0 +1,49 @@ +{ + "config": { + "packageRoots": ["packages/*", "tests/*", "config"], + "changelogRoots": ["packages/*"], + "changelog": { + "labelOrder": [ + ":boom: Breaking Change", + ":evergreen_tree: New Deprecation", + ":memo: Documentation", + ":rocket: Enhancement", + ":bug: Bug Fix", + ":zap: Performance", + ":house: Internal" + ], + "collapseLabels": { + "labels": [":shower: Deprecation Removal", ":goal_net: Test", ":house: Internal"], + "title": ":house: Internal" + }, + "mappings": { + "mock-server": "@warp-drive/diagnostic", + "core": "@warp-drive/core-types", + "Other": null + } + } + }, + "defaults": { + "stage": "stable", + "types": "alpha", + "mirrorPublish": true, + "typesPublish": true + }, + "rules": { + "@ember-data/debug": { + "stage": "stable", + "types": "private", + "typesPublish": false + }, + "@warp-drive/build-config": { + "stage": "alpha", + "types": "alpha", + "typesPublish": false, + "mirrorPublish": true + }, + "@warp-drive/core-types": { + "stage": "alpha", + "types": "alpha" + } + } +} diff --git a/release/tsconfig.json b/release/tsconfig.json new file mode 100644 index 00000000000..f0facb9efeb --- /dev/null +++ b/release/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "noEmit": true, + "types": ["bun-types", "@types/semver"] + } +} diff --git a/release/utils/channel.ts b/release/utils/channel.ts new file mode 100644 index 00000000000..e4fcabc9c01 --- /dev/null +++ b/release/utils/channel.ts @@ -0,0 +1,118 @@ +import semver from 'semver'; + +export type LTS_TAG = `lts-${number}-${number}`; +export type RELEASE_TAG = `release-${number}-${number}`; +export type NPM_DIST_TAG = 'latest' | 'beta' | 'canary' | 'lts' | LTS_TAG | RELEASE_TAG; +export type VALID_BRANCHES = 'main' | 'beta' | 'release' | LTS_TAG | RELEASE_TAG; +export type CHANNEL = 'lts' | 'release' | 'beta' | 'canary' | 'lts-prev' | 'release-prev'; +export type ALPHA_SEMVER = `${number}.${number}.${number}-alpha.${number}`; +export type BETA_SEMVER = `${number}.${number}.${number}-beta.${number}`; +export type RELEASE_SEMVER = `${number}.${number}.${number}`; +export type SEMVER_VERSION = RELEASE_SEMVER | BETA_SEMVER | ALPHA_SEMVER; +/** + * The strategy type is used to determine the next version of a package + * and how to handle types during publish. + * + * For Versions + * + * - alpha means the project is still unstable and we are working towards a beta + * - beta means the project is stable but not yet ready for general release + * - stable means the project is capable of being released for general use + * + * See the `next version` function for more details. + * + * For Types + * + * - private means the project's types are highly unstable and should not be published + * - alpha means the project's types are stable enough to experiment but not recommended. + * Users should expect breaking changes regularly, and must configure their tsconfig + * to consume the types. e.g. `"types": ["ember-data/unstable-preview-types"],` + * - beta means the project's types are stable and can be consumed by the general public + * but are not yet ready for general release. e.g. `"types": ["ember-data/types"],` + * - stable means the project's types are stable and can be consumed by the general public + * no special configuration is required to receive these types, they are the default. + * + * @internal + */ +export type STRATEGY_TYPE = 'stable' | 'alpha' | 'beta'; +export type TYPE_STRATEGY = 'stable' | 'alpha' | 'beta' | 'private'; +export type RELEASE_TYPE = 'major' | 'minor' | 'patch'; + +const RELEASE_BRANCH_REGEXP = /^release\-(\d+)\-(\d+)/; +const LTS_BRANCH_REGEXP = /^lts\-(\d+)\-(\d+)/; + +export function channelForBranch(branch: string, currentVersion: SEMVER_VERSION, force: boolean): CHANNEL { + if (branch === 'main') return 'canary'; + if (branch === 'beta' || branch === 'release' || branch === 'lts') return branch; + if (RELEASE_BRANCH_REGEXP.test(branch)) return 'release-prev'; + if (LTS_BRANCH_REGEXP.test(branch)) return 'lts-prev'; + + if (force) { + if (currentVersion.includes('beta')) { + return 'beta'; + } + if (currentVersion.includes('alpha')) { + return 'canary'; + } + // we wouldn't want to treat this as latest + // unless user is very clear it is + return 'release-prev'; + } + throw new Error(`Attempting to release from an unexpected branch ${branch}`); +} + +export function npmDistTagForChannelAndVersion(channel: CHANNEL, package_version: SEMVER_VERSION): NPM_DIST_TAG { + const major = semver.major(package_version); + const minor = semver.minor(package_version); + + if (major === undefined) { + throw new Error(`Unable to parse semver major from version ${package_version}`); + } + + if (minor === undefined) { + throw new Error(`Unable to parse semver minor from version ${package_version}`); + } + + switch (channel) { + case 'beta': + case 'canary': + case 'lts': + return channel; + case 'release': + return 'latest'; + case 'lts-prev': + return `lts-${major}-${minor}`; + case 'release-prev': + return `release-${major}-${minor}`; + default: + throw new Error(`Unable to determine npm dist-tag for channel ${channel} and version ${package_version}`); + } +} + +export function branchForChannelAndVersion(channel: CHANNEL, package_version: SEMVER_VERSION): VALID_BRANCHES { + const major = semver.major(package_version); + const minor = semver.minor(package_version); + + if (major === undefined) { + throw new Error(`Unable to parse semver major from version ${package_version}`); + } + + if (minor === undefined) { + throw new Error(`Unable to parse semver minor from version ${package_version}`); + } + + switch (channel) { + case 'canary': + return 'main'; + case 'beta': + case 'release': + return channel; + case 'lts': + case 'lts-prev': + return `lts-${major}-${minor}`; + case 'release-prev': + return `release-${major}-${minor}`; + default: + throw new Error(`Unable to determine expected branch name for channel ${channel} and version ${package_version}`); + } +} diff --git a/release/utils/cmd.ts b/release/utils/cmd.ts new file mode 100644 index 00000000000..106de17d381 --- /dev/null +++ b/release/utils/cmd.ts @@ -0,0 +1,195 @@ +import chalk from 'chalk'; +import path from 'path'; +import * as readline from 'readline/promises'; + +type CMD = { + cwd?: string; + cmd: string[] | string; + condense?: boolean; + lines?: number; + silent?: boolean; + env?: Record; +}; + +// async function step() { +// await new Promise((resolve) => setTimeout(resolve, 10)); +// } + +const isCI = Boolean(Bun.env.CI); +class CLICondenser { + declare reader: ReadableStreamDefaultReader; + declare cmd: string; + declare lines: number; + declare cwd: string; + + constructor(cmd: string, reader: ReadableStreamDefaultReader, config: CMD) { + this.reader = reader; + this.cmd = cmd; + this.lines = config.lines ?? 8; + this.cwd = config.cwd ?? process.cwd(); + } + + async read() { + const { reader, cmd, lines } = this; + let output = ''; + let currentLines = 0; + const packets = []; + + const rd = new readline.Readline(process.stdout); + process.stdout.write( + `\n🚀 ${chalk.yellow(cmd)} in ${chalk.greenBright(path.relative(process.cwd(), this.cwd))}\n${chalk.magentaBright( + `⎾` + )}\n` + ); + + // await step(); + while (true) { + let done, value; + try { + const result = await reader.read(); + done = result.done; + value = result.value; + } catch (e) { + throw e; + } + if (done) { + break; + } + + const maxWidth = process.stdout.columns ?? 80; + const maxLines = Math.min(process.stdout.rows, lines); + const packet = new TextDecoder().decode(value, { stream: true }); + packets.push(packet); + output += packet; + const lineOutput = output.split(`\n`); + const lastLines = lineOutput.slice(-maxLines); + const lastLineOutput = lastLines + .map((line) => { + return chalk.magentaBright('⏐ ') + line.substring(0, maxWidth - 2); + }) + .join(`\n`); + + if (!isCI && currentLines) { + // process.stdout.write(`\tclearing ${currentLines} lines`); + // await step(); + rd.cursorTo(0); + // await rd.commit(); + // await step(); + while (currentLines--) { + rd.clearLine(0); + // await rd.commit(); + // await step(); + rd.moveCursor(0, currentLines === 0 ? 0 : -1); + // await rd.commit(); + // await step(); + } + await rd.commit(); + } + + currentLines = lastLines.length + 1; + process.stdout.write(lastLineOutput + '\n' + chalk.magentaBright('⎿')); + if (isCI) { + process.stdout.write('\n'); + } + // await step(); + } + + if (!isCI) { + currentLines = currentLines + 3; + // process.stdout.write(`\tclearing ${currentLines} lines`); + // await step(); + rd.cursorTo(0); + // await rd.commit(); + // await step(); + while (currentLines--) { + rd.clearLine(0); + // await rd.commit(); + // await step(); + rd.moveCursor(0, currentLines === 0 ? 0 : -1); + // await rd.commit(); + // await step(); + } + await rd.commit(); + } + process.stdout.write( + `\t☑️\t${chalk.grey(cmd)} in ${chalk.greenBright(path.relative(process.cwd(), this.cwd) || '')}\n` + ); + + return output; + } +} + +/** + * + * @see {@link CMD} + * + * @internal + */ +export async function exec(cmd: string[] | string | CMD, dryRun: boolean = false) { + const isCmdWithConfig = typeof cmd === 'object' && !Array.isArray(cmd); + const mainCommand = isCmdWithConfig ? cmd.cmd : cmd; + const cwd = isCmdWithConfig && cmd.cwd ? cmd.cwd : process.cwd(); + + let args = mainCommand; + if (typeof args === 'string') { + args = args.split(' '); + } + + if (dryRun) { + console.log(`\t` + chalk.grey(`Would Run: ${Array.isArray(mainCommand) ? mainCommand.join(' ') : mainCommand}`)); + } else if (!isCmdWithConfig || (!cmd.condense && !cmd.silent)) { + console.log( + `\t` + chalk.grey(`Running: ${args.join(' ')} in ${chalk.green(path.relative(process.cwd(), cwd))}\t...`) + ); + } + + if (!dryRun) { + if (isCmdWithConfig && cmd.condense) { + const proc = Bun.spawn(args, { + env: cmd.env || process.env, + cwd, + stderr: 'pipe', + stdout: 'pipe', + }); + + const reader = proc.stdout.getReader() as ReadableStreamDefaultReader; + const condenser = new CLICondenser(args.join(' '), reader, cmd); + const result = await condenser.read(); + + await proc.exited; + if (proc.exitCode !== 0) { + console.log(result); + const errText = await new Response(proc.stderr).text(); + console.error('\t' + errText.split('\n').join('\n\t')); + throw proc.exitCode; + } + return result; + } else { + const proc = Bun.spawn(args, { + env: process.env, + cwd, + stderr: 'pipe', + stdout: 'pipe', + }); + + await proc.exited; + if (proc.exitCode !== 0) { + const logText = await new Response(proc.stdout).text(); + const errText = await new Response(proc.stderr).text(); + console.error('\t' + errText.split('\n').join('\n\t')); + + const error = new Error(`exit code: ${String(proc.exitCode)}`); + // @ts-expect-error - adding properties to custom Error + error.logText = logText; + // @ts-expect-error - adding properties to custom Error + error.errText = errText; + + throw error; + } + + return await new Response(proc.stdout).text(); + } + } else { + return ''; + } +} diff --git a/release/utils/flags-config.ts b/release/utils/flags-config.ts new file mode 100644 index 00000000000..50fdb5740af --- /dev/null +++ b/release/utils/flags-config.ts @@ -0,0 +1,446 @@ +import { HELP } from '../help/sections/manual'; +import { ABOUT } from '../help/sections/about'; +import { normalizeFlag, type CommandConfig, type FlagConfig } from './parse-args'; +import { CHANNEL, SEMVER_VERSION, npmDistTagForChannelAndVersion } from './channel'; +import { getGitState, getPublishedChannelInfo } from './git'; +import chalk from 'chalk'; +import semver from 'semver'; + +/** + * Like Pick but returns an object type instead of a union type. + * + * @internal + */ +type Subset = { + [P in K]: T[P]; +}; + +/** + * Like Typescript Pick but For Runtime. + * + * @internal + */ +export function pick, K extends keyof T>(obj: T, keys: K[]): Subset { + const result = {} as Subset; + + for (const key of keys) { + result[key] = obj[key]; + } + + return result; +} + +/** + * Like Object.assign (is Object.assign) but ensures each arg and the result conform to T + * + * @internal + */ +export function merge(...args: T[]): T { + return Object.assign({}, ...args); +} + +export const publish_flags_config: FlagConfig = { + help: { + name: 'Help', + flag: 'help', + flag_aliases: ['h', 'm'], + flag_mispellings: [ + 'desc', + 'describe', + 'doc', + 'docs', + 'dsc', + 'guide', + 'halp', + 'he', + 'hel', + 'hlp', + 'man', + 'mn', + 'usage', + ], + type: Boolean, + default_value: false, + description: 'Print this usage manual.', + examples: ['./publish/index.ts --help'], + }, + channel: { + name: 'Channel', + flag: 'channel', + type: String, + default_value: async (options: Map) => { + const gitState = await getGitState(options); + return gitState.expectedChannel; + }, + validate: (value: unknown) => { + if (!['lts', 'release', 'beta', 'canary', 'lts-prev', 'release-prev'].includes(value as string)) { + throw new Error(`Channel must be one of lts, release, beta, canary, lts-prev, or release-prev. Got ${value}`); + } + }, + description: + 'EmberData always publishes to a "release channel".\nTypically this will be one of lts, release, beta, or canary.\nWhen publishing a new version of a non-current lts or non-current release, the channel should be "lts-prev" or "release-prev"', + examples: ['./publish/index.ts lts', './publish/index.ts publish lts', './publish/index.ts --channel=lts'], + positional: true, + positional_index: 0, + // required: true, + }, + dry_run: { + name: 'Dry Run', + flag: 'dry_run', + flag_mispellings: ['dry'], + default_value: false, + description: 'Do not actually publish, just print what would be done', + type: Boolean, + examples: ['./publish/index.ts --channel=stable --dry_run'], + }, + dangerously_force: { + name: 'Force Release', + flag: 'dangerously_force', + flag_mispellings: [], + default_value: false, + description: 'Ignore safety checks and attempt to create and publish a release anyway', + type: Boolean, + examples: ['./publish/index.ts --channel=stable --dangerously_force'], + }, + tag: { + name: 'NPM Distribution Tag', + flag: 'tag', + flag_aliases: ['t'], + flag_mispellings: ['dist_tag'], + type: String, + description: '', + examples: [], + default_value: async (options: Map) => { + const gitInfo = await getGitState(options); + return npmDistTagForChannelAndVersion(gitInfo.expectedChannel, gitInfo.rootVersion); + }, + validate: async (value: unknown, options: Map) => { + const channel = options.get('channel') as CHANNEL; + const gitInfo = await getGitState(options); + const expectedTag = npmDistTagForChannelAndVersion(channel, gitInfo.rootVersion); + if (value !== expectedTag) { + if (!options.get('dangerously_force')) { + throw new Error( + `Expected npm dist-tag ${expectedTag} for channel ${channel} on branch ${gitInfo.branch} with version ${gitInfo.rootVersion} but got ${value}` + ); + } else { + console.log( + chalk.red( + `\t🚨 Expected npm dist-tag ${expectedTag} for channel ${channel} on branch ${ + gitInfo.branch + } with version ${gitInfo.rootVersion} but got ${value}\n\t\t${chalk.yellow( + '⚠️ Continuing Due to use of --dangerously-force' + )}` + ) + ); + } + } + }, + }, + increment: { + name: 'Version Increment', + flag: 'increment', + flag_aliases: ['i', 'b'], + flag_mispellings: ['inc', 'bump', 'incr'], + description: 'kind of version bump to perform, if any.\nMust be one of "major", "minor", or "patch"', + type: String, + examples: [], + default_value: 'patch', + validate: (value: unknown) => { + if (!['major', 'minor', 'patch'].includes(value as string)) { + throw new Error(`the 'increment' option must be one of 'major', 'minor' or 'patch'`); + } + }, + }, + commit: { + name: 'Commit', + flag: 'commit', + flag_aliases: ['c'], + flag_mispellings: ['cm', 'comit', 'changelog', 'commit_changelog'], + description: 'Whether to commit the changes to the changelogs', + type: Boolean, + examples: [], + default_value: true, + }, + // branch: { + // name: 'Update Local and Upstream Branch', + // flag: 'update_branch', + // flag_aliases: [], + // flag_mispellings: ['branch'], + // description: + // 'Whether to update the local and upstream branch according to the standard release channel flow. For release this will reset the branch to the current beta. For beta this will reset the branch to the current canary. For lts this will reset the branch to the current release. For lts-prev this is not a valid option.', + // type: Boolean, + // examples: [], + // default_value: false, + // }, + from: { + name: 'From Version', + flag: 'from', + flag_aliases: ['v'], + flag_mispellings: ['ver'], + description: 'The version from which to increment and build a strategy', + type: String, + examples: [], + default_value: async (options: Map) => { + const channel = options.get('channel') as CHANNEL; + if (channel === 'lts' || channel === 'release' || channel === 'beta') { + const version = (await getPublishedChannelInfo())[channel === 'release' ? 'latest' : channel]; + const currentVersion = (await getGitState(options)).rootVersion; + if (version !== currentVersion) { + return version; + } + return ''; + } + return ''; + }, + validate: async (value: unknown) => { + if (typeof value !== 'string') { + throw new Error(`Expected a string but got ${value}`); + } + if (value.startsWith('v')) { + throw new Error(`Version passed to promote should not start with 'v'`); + } + if (semver.valid(value) === null) { + throw new Error(`Version passed to promote is not a valid semver version`); + } + }, + }, + upstream: { + name: 'Update Upstream Branch', + flag: 'upstream', + flag_aliases: ['u'], + flag_mispellings: ['upstraem', 'up'], + description: 'Whether to push the commits and tag upstream', + type: Boolean, + examples: [], + default_value: true, + }, + pack: { + name: 'Pack Packages', + flag: 'pack', + flag_aliases: ['p'], + flag_mispellings: ['skip-pack'], + description: 'whether to pack tarballs for the public packages', + type: Boolean, + examples: [], + default_value: true, + }, + publish: { + name: 'Publish Packages to NPM', + flag: 'publish', + flag_aliases: ['r'], + flag_mispellings: ['skip-publish', 'skip-release', 'release'], + description: 'whether to publish the packed tarballs to the npm registry', + type: Boolean, + examples: [], + default_value: true, + }, +}; + +export const release_notes_flags_config: FlagConfig = merge( + pick(publish_flags_config, ['help', 'increment', 'dry_run', 'dangerously_force', 'tag', 'channel', 'upstream']), + { + commit: { + name: 'Commit', + flag: 'commit', + flag_aliases: ['c'], + flag_mispellings: ['cm', 'comit'], + description: 'Whether to commit the changes to the changelogs', + type: Boolean, + examples: [], + default_value: true, + }, + from: { + name: 'From Version', + flag: 'from', + flag_aliases: ['v'], + flag_mispellings: ['ver', 'release', 'rel'], + description: 'The version from which to increment and build a strategy', + type: String, + examples: [], + default_value: async (options: Map) => { + return (await getPublishedChannelInfo()).latest; + }, + validate: async (value: unknown) => { + if (typeof value !== 'string') { + throw new Error(`Expected a string but got ${value}`); + } + if (value.startsWith('v')) { + throw new Error(`Version passed to promote should not start with 'v'`); + } + if (semver.valid(value) === null) { + throw new Error(`Version passed to promote is not a valid semver version`); + } + const versionInfo = semver.parse(value); + if (versionInfo?.prerelease?.length) { + throw new Error(`Version passed to promote cannot be prerelease version`); + } + }, + }, + } +); + +export const promote_flags_config: FlagConfig = merge( + pick(publish_flags_config, ['help', 'dry_run', 'dangerously_force', 'upstream']), + { + version: { + name: 'Version', + flag: 'version', + flag_aliases: ['v'], + flag_mispellings: ['ver', 'release', 'rel'], + description: 'The version to promote to LTS', + type: String, + examples: [], + default_value: async (options: Map) => { + return (await getPublishedChannelInfo()).latest; + }, + validate: async (value: unknown) => { + if (typeof value !== 'string') { + throw new Error(`Expected a string but got ${value}`); + } + if (value.startsWith('v')) { + throw new Error(`Version passed to promote should not start with 'v'`); + } + if (semver.valid(value) === null) { + throw new Error(`Version passed to promote is not a valid semver version`); + } + const versionInfo = semver.parse(value); + if (versionInfo?.prerelease?.length) { + throw new Error(`Version passed to promote cannot be prerelease version`); + } + }, + }, + tag: { + name: 'NPM Distribution Tag', + flag: 'tag', + flag_aliases: ['t'], + flag_mispellings: ['dist_tag'], + type: String, + description: '', + examples: [], + default_value: async (options: Map) => { + const version = options.get('version') as SEMVER_VERSION; + const existing = await getPublishedChannelInfo(); + + if (existing.latest === version) { + return 'lts'; + } else { + return npmDistTagForChannelAndVersion('lts-prev', version); + } + }, + validate: async (value: unknown, options: Map) => { + let version = options.get('version') as SEMVER_VERSION; + const existing = await getPublishedChannelInfo(); + + if (!version) { + version = (await getPublishedChannelInfo()).latest; + } + + if (value !== 'lts') { + // older lts channels should match lts-- + if (typeof value !== 'string' || !value.startsWith('lts-')) { + throw new Error(`Expected a tag starting with "lts-" but got ${value}`); + } + + const expected = npmDistTagForChannelAndVersion('lts-prev', version); + + if (expected !== value) { + throw new Error(`Expected tag lts or ${expected} for version ${version} but got ${value}`); + } + } + + if (existing[value] === version) { + throw new Error(`Version ${version} is already published to ${value}`); + } + + const current = existing[value]; + if (current && semver.lt(version, current)) { + throw new Error(`Version ${version} is less than the latest version ${current}`); + } + }, + }, + } +); + +export const command_config: CommandConfig = { + help: { + name: 'Help', + cmd: 'help', + description: 'Output This Manual', + alt: Array.from(HELP), + example: '$ bun release help', + }, + exec: { + name: 'Execute Command', + cmd: 'exec', + description: + 'Executes another release command with the provided arguments, filtering out any args with undefined values.', + alt: [], + example: '$ bun release exec promote --version=5.3.0 --tag=lts', + }, + about: { + name: 'About', + cmd: 'about', + description: 'Print Information About This Script', + alt: Array.from(ABOUT), + example: '$ bun release about', + }, + release_notes: { + name: 'Release Notes', + cmd: 'release-notes', + alt: ['cl', 'changes', 'history', 'notes', 'releasenotes', 'changelog', 'log'], + description: `Generate release notes for the next release.`, + options: release_notes_flags_config, + example: '$ bun release cl', + }, + latest_for: { + name: 'Latest For', + cmd: 'latest-for', + description: 'Print the latest version for a given channel', + alt: ['latest'], + example: '$ bun release latest-for beta', + }, + promote: { + name: 'Promote to LTS', + cmd: 'promote', + description: + 'Promote a prior release to LTS.\nThis will upate the dist-tags on npm without publishing any new versions or tarballs', + alt: ['retag', 'lts', 'lts-promote'], + options: promote_flags_config, + example: [ + '$ bun release promote', + '$ bun release promote --version=5.3.0 --tag=lts', + '$ bun release promote 4.12.5 --tag=lts-4-12', + ], + }, + default: { + name: 'Publish', + cmd: 'publish', + default: true, + description: + 'Publish a new version of EmberData to the specified channel.\nRequires a configured ye<> with npm access to all associated scopes and packages,\nor the ability to generate an OTP token for the same.', + options: publish_flags_config, + example: ['$ bun release', '$ bun release publish'], + }, +}; + +export function getCommands() { + const keys = Object.keys(command_config); + const commands = new Map(); + keys.forEach((key) => { + const cmd = normalizeFlag(key); + commands.set(cmd, cmd); + commands.set(command_config[key].cmd, cmd); + if (command_config[cmd].alt) { + command_config[cmd].alt!.forEach((alt: string) => { + const alternate = normalizeFlag(alt); + if (commands.has(alternate) && commands.get(alternate) !== cmd) { + throw new Error(`Duplicate command alias ${alternate} for ${cmd} and ${commands.get(alternate)}`); + } + commands.set(alternate, cmd); + }); + } + }); + + return commands; +} diff --git a/release/utils/git.ts b/release/utils/git.ts new file mode 100644 index 00000000000..98e07a43878 --- /dev/null +++ b/release/utils/git.ts @@ -0,0 +1,227 @@ +import chalk from 'chalk'; +import { + branchForChannelAndVersion, + CHANNEL, + channelForBranch, + npmDistTagForChannelAndVersion, + SEMVER_VERSION, + VALID_BRANCHES, +} from './channel'; +import { getFile } from './json-file'; +import { exec } from './cmd'; +import { gatherPackages, loadStrategy, Package } from './package'; +import path from 'path'; + +export type LTS_TAG = `lts-${number}-${number}`; +export type RELEASE_TAG = `release-${number}-${number}`; +export type GIT_TAG = + | `v${number}.${number}.${number}` + | `v${number}.${number}.${number}-alpha.${number}` + | `v${number}.${number}.${number}-beta.${number}`; + +export type CHANNEL_VERSIONS = { + latest: SEMVER_VERSION; + beta: SEMVER_VERSION; + canary: SEMVER_VERSION; + lts: SEMVER_VERSION; + [key: LTS_TAG | RELEASE_TAG]: SEMVER_VERSION | undefined; +}; + +export type GIT_STATE = { + rootVersion: SEMVER_VERSION; + isClean: boolean; + isCurrent: boolean; + isCorrectBranch: boolean; + branch: string; + expectedBranch: VALID_BRANCHES; + expectedChannel: CHANNEL; +}; + +let _NPM_INFO: Record | null = null; +export async function getPublishedChannelInfo(options?: { silent: boolean }): Promise { + if (!_NPM_INFO) { + const gitInfo = await exec({ + cmd: ['npm', 'view', 'ember-data@latest', '--json'], + silent: options?.silent ?? false, + }); + _NPM_INFO = JSON.parse(gitInfo) as Record; + } + return _NPM_INFO['dist-tags'] as CHANNEL_VERSIONS; +} + +let _GIT_STATE: GIT_STATE | null = null; +export async function getGitState(options: Map): Promise { + if (_GIT_STATE) { + return _GIT_STATE; + } + const dangerously_force = options.get('dangerously_force') as boolean; + const isHelp = options.get('help') as boolean; + const status = await exec(['git', 'status']); + let clean = true; + let current = true; + + if (!status.match(/^nothing to commit/m)) { + clean = false; + if (dangerously_force || isHelp) { + const base = chalk.white('\t⚠️ Local Git branch has uncommitted changes!'); + console.log( + dangerously_force + ? base + + chalk.yellow('\n\t\tPassed option: ') + + chalk.white('--dangerously-force') + + chalk.grey(' :: ignoring unclean git working tree') + : base + ); + if (!isHelp) { + await exec('git add -A'); + await exec(['git', 'commit', '-m', '"publish: stash of uncommitted changes by release script"']); + } + } else { + console.log( + chalk.red('💥 Git working tree is not clean. 💥 \n\t') + + chalk.grey('Use ') + + chalk.white('--dangerously-force') + + chalk.grey(' to ignore this warning and publish anyway\n') + + chalk.yellow('⚠️ Publishing from an unclean working state may result in a broken release ⚠️\n\n') + + chalk.grey(`Status:\n${status}`) + ); + process.exit(1); + } + + if (!status.match(/^Your branch is up to date with/m)) { + current = false; + if (dangerously_force || isHelp) { + const base = chalk.white('\t⚠️ Local Git branch is not in sync with origin branch'); + console.log( + dangerously_force + ? base + + chalk.yellow('\n\t\tPassed option: ') + + chalk.white('--dangerously-force') + + chalk.grey(' :: ignoring unsynced git branch') + : base + ); + } else { + console.log( + chalk.red('💥 Local Git branch is not in sync with origin branch. 💥 \n\t') + + chalk.grey('Use ') + + chalk.white('--dangerously-force') + + chalk.grey(' to ignore this warning and publish anyway\n') + + chalk.yellow('⚠️ Publishing from an unsynced working state may result in a broken release ⚠️') + + chalk.grey(`Status:\n${status}`) + ); + process.exit(1); + } + } + } + + const rootPkg = await getFile<{ version: SEMVER_VERSION }>(`${process.cwd()}/package.json`).read(); + const rootVersion = rootPkg.version; + + const foundBranch = status.split('\n')[0].replace('On branch ', ''); + const channel = + (options.get('channel') as CHANNEL) || channelForBranch(foundBranch, rootVersion, dangerously_force || isHelp); + const expectedBranch = branchForChannelAndVersion(channel, rootVersion); + + if (foundBranch !== expectedBranch) { + if (dangerously_force || isHelp) { + const base = chalk.white( + `\t⚠️ Expected to publish the release-channel '${channel}' from the git branch '${expectedBranch}', but found '${foundBranch}'` + ); + console.log( + dangerously_force + ? base + + chalk.yellow('\n\t\tPassed option: ') + + chalk.white('--dangerously-force') + + chalk.grey(' :: ignoring unexpected branch') + : base + ); + } else { + console.log( + chalk.red( + `💥 Expected to publish the release-channel '${channel}' from the git branch '${expectedBranch}', but found '${foundBranch}' 💥 \n\t` + ) + + chalk.grey('Use ') + + chalk.white('--dangerously-force') + + chalk.grey(' to ignore this warning and publish anyway\n') + + chalk.yellow('⚠️ Publishing from an incorrect branch may result in a broken release ⚠️') + ); + process.exit(1); + } + } + + _GIT_STATE = { + rootVersion, + isClean: clean, + isCurrent: current, + isCorrectBranch: foundBranch === expectedBranch, + branch: foundBranch, + expectedBranch, + expectedChannel: channel, + }; + return _GIT_STATE; +} + +// currently we support things like +// "./tmp/v5.3.2/.editorconfig: Failed to restore metadata\ntar: Error exit delayed from previous errors.\n" +// because extraction succeeds even when metadata is not restored +// we may potentially want to check that the file that had the error did extract +// to ensure this logic is sound +async function isUnrecoverableExtractionError(e: Error): Promise { + const { errText } = e as unknown as { errText: string }; + const errors = errText.trim().split('\n'); + const lastError = errors.pop(); + + if (lastError !== 'tar: Error exit delayed from previous errors.') { + return true; + } + + for (const error of errors) { + if (!error.includes('Failed to restore metadata')) { + return true; + } + } + + // if we have handled all errors during iteration + // and reach here then we are recoverable. + return false; +} + +export async function getAllPackagesForGitTag(tag: GIT_TAG): Promise> { + const relativeTmpDir = `./tmp/${tag}/`; + await exec(['mkdir', '-p', relativeTmpDir]); + try { + await exec({ cmd: ['sh', '-c', `git archive ${tag} --prefix ${relativeTmpDir} | tar -x`] }); + } catch (e) { + if (await isUnrecoverableExtractionError(e as unknown as Error)) { + console.log(chalk.red(`🔴 Failed to extract git tag ${tag} to ${relativeTmpDir}`)); + throw e; + } else { + console.log(chalk.yellow(`\t⚠️ Recovered from errors during extraction of ${tag} to ${relativeTmpDir}`)); + } + } + const tmpDir = path.join(process.cwd(), relativeTmpDir); + try { + const strategy = await loadStrategy(tmpDir); + return gatherPackages(strategy.config, tmpDir); + } catch (e) { + // if strategy does not exist we may be pre-strategy days + // so we will just gather all packages from the packages directory + + return gatherPackages({ packageRoots: ['packages/*'] }, tmpDir); + } +} + +export async function pushLTSTagToRemoteBranch(tag: GIT_TAG, force?: boolean): Promise { + const sha = await exec({ cmd: `git rev-list -n 1 ${tag}` }); + const branch = npmDistTagForChannelAndVersion('lts-prev', tag.slice(1) as SEMVER_VERSION); + let oldSha = ''; + try { + oldSha = await exec({ cmd: `git rev-list -n 1 refs/heads/${branch}` }); + } catch { + // no-op, branch does not exist + } + let cmd = `git push origin refs/tags/${tag}:refs/heads/${branch}`; + if (force) cmd += ' -f'; + await exec({ cmd }); + console.log(chalk.green(`✅ Pushed ${tag} to ${branch} (${oldSha.slice(0, 10)} => ${sha.slice(0, 10)})`)); +} diff --git a/release/utils/json-file.ts b/release/utils/json-file.ts new file mode 100644 index 00000000000..fc588144fb2 --- /dev/null +++ b/release/utils/json-file.ts @@ -0,0 +1,72 @@ +import { BunFile } from 'bun'; +import chalk from 'chalk'; + +const EOL = '\n'; +export class JSONFile> { + declare contents: T | null; + declare filePath: string; + declare handle: BunFile; + + #lastKnown: string | null = null; + + constructor(filePath: string) { + this.contents = null; + this.filePath = filePath; + } + + async #getHandle() { + if (!this.handle) { + const fileHandle = Bun.file(this.filePath, { type: 'application/json' }); + const exists = await fileHandle.exists(); + + if (!exists) { + throw new Error(`The file ${chalk.white(this.filePath)} does not exist!`); + } + + this.handle = fileHandle; + } + + return this.handle; + } + + async invalidate() { + this.contents = null; + } + + async read(): Promise { + if (this.contents === null) { + const fileHandle = await this.#getHandle(); + const data = await fileHandle.json(); + this.contents = data; + this.#lastKnown = JSON.stringify(data, null, 2); + } + + return this.contents; + } + + async write(allowNoop?: boolean): Promise { + if (this.contents === null) { + throw new Error(`Cannot write before updating contents`); + } + const strData = JSON.stringify(this.contents, null, 2) + EOL; + if (this.#lastKnown === strData) { + if (allowNoop) { + return; + } + throw new Error(`Should not write when not updating contents`); + } + this.#lastKnown = strData; + const fileHandle = await this.#getHandle(); + await Bun.write(fileHandle, strData); + } +} + +const FILES: Map = new Map(); + +export function getFile(filePath: string): JSONFile { + let file: JSONFile | undefined = FILES.get(filePath) as JSONFile; + if (!file) { + file = new JSONFile(filePath); + } + return file; +} diff --git a/release/utils/package.ts b/release/utils/package.ts new file mode 100644 index 00000000000..9a0b1387575 --- /dev/null +++ b/release/utils/package.ts @@ -0,0 +1,154 @@ +import { JSONFile, getFile } from './json-file'; +import { NPM_DIST_TAG, SEMVER_VERSION, STRATEGY_TYPE, TYPE_STRATEGY } from './channel'; +import { Glob } from 'bun'; +import path from 'path'; +export class Package { + declare filePath: string; + declare file: JSONFile; + declare pkgData: PACKAGEJSON; + declare tarballPath: string; + declare mirrorTarballPath: string; + declare typesTarballPath: string; + + constructor(filePath: string, file: JSONFile, pkgData: PACKAGEJSON) { + this.filePath = filePath; + this.file = file; + this.pkgData = pkgData; + this.tarballPath = ''; + this.mirrorTarballPath = ''; + this.typesTarballPath = ''; + } + + async refresh() { + await this.file.invalidate(); + this.pkgData = await this.file.read(); + } +} + +/** + * A valid package.json file can go up to 3 levels deep + * when defining the exports field. + * + * ``` + * { + * "exports": { + * ".": "./index.js", + * "main": { + * "import": "./index.js", + * "require": "./index.js" + * "browser": { + * "import": "./index.js", + * "require": "./index.js" + * } + * } + * } + * } + * ``` + * + * @internal + */ +type ExportConfig = Record>>; + +export type PACKAGEJSON = { + name: string; + version: SEMVER_VERSION; + private: boolean; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + scripts?: Record; + files?: string[]; + exports?: ExportConfig; + 'ember-addon'?: { + main?: 'addon-main.js'; + type?: 'addon'; + version?: 1 | 2; + }; + author?: string; + license?: string; + repository?: { + type: string; + url: string; + directory?: string; + }; +}; + +export type APPLIED_STRATEGY = { + name: string; + private: boolean; + stage: STRATEGY_TYPE; + types: TYPE_STRATEGY; + mirrorPublish: boolean; + mirrorPublishTo: string; + typesPublish: boolean; + typesPublishTo: string; + fromVersion: SEMVER_VERSION; + toVersion: SEMVER_VERSION; + distTag: NPM_DIST_TAG; + pkgDir: string; + new: boolean; +}; + +export interface STRATEGY { + config: { + packageRoots: string[]; + changelogRoots?: string[]; + changelog?: { + collapseLabels?: { + labels: string[]; + title: string; + }; + labelOrder?: string[]; + mappings: Record; + }; + }; + defaults: { + stage: STRATEGY_TYPE; + types: TYPE_STRATEGY; + mirrorPublish?: boolean; + typesPublish?: boolean; + }; + rules: Record< + string, + { + stage: STRATEGY_TYPE; + types: TYPE_STRATEGY; + mirrorPublish?: boolean; + typesPublish?: boolean; + } + >; +} + +function buildGlob(dirPath: string) { + return `${dirPath}/package.json`; +} + +export async function gatherPackages(config: STRATEGY['config'], cwd: string = process.cwd()) { + const packages: Map = new Map(); + + // add root + const rootFilePath = `${cwd}/package.json`; + const rootFile = getFile(rootFilePath); + const rootPkgData = await rootFile.read(); + packages.set('root', new Package(rootFilePath, rootFile, rootPkgData)); + + // add other packages + for (const dirPath of config.packageRoots) { + const glob = new Glob(buildGlob(dirPath)); + + // Scans the current working directory and each of its sub-directories recursively + for await (const filePath of glob.scan(cwd)) { + const file = getFile(path.join(cwd, filePath)); + const pkgData = await file.read(); + packages.set(pkgData.name, new Package(filePath, file, pkgData)); + } + } + + return packages; +} + +export async function loadStrategy(cwd: string = process.cwd()) { + const file = getFile(`${cwd}/release/strategy.json`); + const data = await file.read(); + return data; +} diff --git a/release/utils/parse-args.ts b/release/utils/parse-args.ts new file mode 100644 index 00000000000..baf79fc4666 --- /dev/null +++ b/release/utils/parse-args.ts @@ -0,0 +1,323 @@ +export type Command = { + name: string; + cmd: string; + description: string; + example?: string | string[]; + overview?: string; + default?: boolean; + alt?: string[]; + options?: FlagConfig; +}; +export type CommandConfig = Record; +export type Flag = { + name: string; + flag: string; + flag_aliases?: string[]; + flag_mispellings?: string[]; + description: string; + validate?: (value: unknown, config: Map) => void | Promise; + examples: Array< + | string + | { + desc: string; + example: string[]; + } + >; + type: StringConstructor | NumberConstructor | BooleanConstructor; + default_value?: + | string + | number + | boolean + | null + | (( + config: Map + ) => string | number | boolean | null | Promise); + /* + Positional flags are not specified by name, but by position + When using this with more than one positional flag, you must specify positional_index + */ + positional?: boolean; + /* + Positional flags must be specified in order + If you have two positional flags, you must specify positional_index + + Discovered positional values will be mapped to flags based on their relative + index. E.g. first positional value discovered is `0`, second is `1` and so on. + */ + positional_index?: number; + /* + Required flags must be specified by the user, and will throw an error if not + */ + required?: boolean; + required_error?: string; + /* + If a boolean flag is present AND does not have an explicitly set value, its value + is false instead of true + If a boolean flag is not present, the default_value is used if one is provided, + else the value is true (instead of false) + + e.g. + + `--some-bool` -> false + `--some-bool=true` -> true + `--some-bool=1` -> true + `--some-bool=false` -> false + `--some-bool=0` -> false + */ + invert_boolean?: boolean; +}; +export interface FlagConfig { + [key: string]: Flag; +} + +const FalseyStrings = new Set(['false', '0', 'no', 'n', 'off', '']); + +function processRawValue(config: Flag, raw_value: string | undefined): string | number | boolean | null { + if (raw_value === undefined) { + if (config.type === Boolean) { + return config.invert_boolean ? false : true; + } else if (config.default_value !== undefined && typeof config.default_value !== 'function') { + return config.default_value; + } + raw_value = ''; + } + + if (config.type === Boolean) { + return !FalseyStrings.has(raw_value.toLowerCase()); + } else if (config.type === Number) { + return Number(raw_value); + } else { + return raw_value; + } +} + +async function processMissingFlag( + config: Flag, + values: Map +): Promise { + if (config.default_value !== undefined) { + if (typeof config.default_value === 'function') { + return await config.default_value(values); + } + return config.default_value; + } else if (config.type === Boolean && config.invert_boolean) { + return true; + } else { + throw new Error(`Flag ${config.name} (${config.flag}) had no default value and was not provided by the user`); + } +} + +/** + * process the config to create mappings for aliases and misspellings + */ +function createMappings(flags_config: FlagConfig): { + aliases: Map; + spellings: Map; + positional: Flag[]; + all: Map; +} { + const aliases = new Map(); + const spellings = new Map(); + const seen_positions = new Set(); + const positional: Flag[] = []; + const all = new Map(); + Object.keys(flags_config).forEach((f) => { + const flag = normalizeFlag(f); + const config = flags_config[f]; + + if (config.flag !== flag) { + throw new Error(`Expected configuration key ${flag} for ${config.name} to match ${config.flag}`); + } + all.set(flag, config); + + // TODO validate flag config structure more thoroughly + // esp for non-optional fields + + if (config.positional) { + if (typeof config.positional_index !== 'number') { + throw new Error(`Positional flag ${config.name} must specify positional_index in its config`); + } + if (seen_positions.has(config.positional_index)) { + throw new Error(`Positional flag ${config.name} has a duplicate positional_index`); + } + seen_positions.add(config.positional_index); + positional.push(config); + } + + if (Array.isArray(config.flag_aliases)) { + config.flag_aliases.forEach((a) => { + const alias = normalizeFlag(a); + if (alias.length !== 1) { + throw new Error(`Flag aliases must be a single character, found ${alias} for ${flag}`); + } + if (aliases.has(alias)) { + throw new Error(`Alias ${alias} is already in use by ${aliases.get(alias)}`); + } + aliases.set(alias, flag); + }); + } + + // always add ourself to the spellings map + spellings.set(flag, flag); + if (Array.isArray(config.flag_mispellings)) { + config.flag_mispellings.forEach((msp) => { + const misspelling = normalizeFlag(msp); + if (misspelling.length < 2) { + throw new Error(`Flag misspellings must be at least two characters, found ${misspelling} for ${flag}`); + } + if (spellings.has(misspelling)) { + throw new Error(`Misspelling ${misspelling} is already in use by ${spellings.get(misspelling)}`); + } + spellings.set(misspelling, flag); + }); + } + }); + + positional.sort((a, b) => { + return a.positional_index! > b.positional_index! ? 1 : -1; + }); + + return { aliases, spellings, positional, all }; +} + +/** + * normalize a string to lowercase and replace dashes with underscores + * + */ +export function normalizeFlag(str: string): string { + let normalized = str + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replaceAll('-', '_'); + + while (normalized.charAt(0) === '_') { + normalized = normalized.slice(1); + } + + return normalized; +} + +/** + * Process raw user provided command line arguments into a populated config object + */ +export async function parseRawFlags( + raw: string[], + flags_config: FlagConfig +): Promise<{ + specified: Map; + full: Map; +}> { + let current_position = 0; + const processed_flags = new Map(); + const { aliases, spellings, positional, all } = createMappings(flags_config); + + for (let i = 0; i < raw.length; i++) { + const raw_arg = raw[i]; + + // handle named args + if (raw_arg.startsWith('--')) { + const arg = raw_arg.slice(2); + const parts = arg.split('='); + const spelling = normalizeFlag(parts[0]); + const flag = spellings.get(spelling) || aliases.get(spelling); + if (!flag) { + throw new Error(`Unknown flag: ${spelling}`); + } + const config = flags_config[flag]; + let raw_value = parts[1]; + + if (config) { + if (processed_flags.has(flag)) { + throw new Error(`Flag ${flag} was provided more than once`); + } + + // scan ahead for a value + // scan ahead is not valid for boolean flags + if (raw_value === undefined && config.type !== Boolean) { + const potential_value = raw[i + 1]; + if (potential_value && !potential_value.startsWith('-')) { + raw_value = potential_value; + i++; + } + } + + processed_flags.set(flag, processRawValue(config, raw_value)); + } else { + throw new Error(`Unknown flag: ${flag}`); + } + + // treat as aliases + } else if (raw_arg.startsWith('-')) { + const arg = normalizeFlag(raw_arg.slice(1)); + // we only allow one non-boolean flag per alias group + let has_found_non_boolean_flag = false; + + for (let j = 0; j < arg.length; j++) { + const alias = arg[j]; + const flag = aliases.get(alias); + if (!flag) { + throw new Error(`Unknown flag alias: ${alias}`); + } + const config = flags_config[flag]; + if (!config) { + throw new Error(`Unknown flag: ${flag} found for alias ${alias}`); + } + if (processed_flags.has(flag)) { + throw new Error(`Flag ${flag} was provided more than once (discovered via alias '${alias}')`); + } + let raw_value: string | undefined = undefined; + if (config.type !== Boolean) { + if (has_found_non_boolean_flag) { + throw new Error(`An alias group may only contain one non-boolean flag alias`); + } + + // scan ahead for the value + const potential_value = raw[i + 1]; + if (potential_value && !potential_value.startsWith('-')) { + raw_value = potential_value; + i++; + } else { + throw new Error( + `The non-boolean flag alias ${alias} was provided for ${flag} without a corresponding value as the next argument` + ); + } + + has_found_non_boolean_flag = true; + } + processed_flags.set(flag, processRawValue(config, raw_value)); + } + + // treat as positional + } else { + const config = positional[current_position++]; + if (!config) { + throw new Error(`Unknown positional argument: ${raw_arg}`); + } + + const value = processRawValue(config, raw_arg); + processed_flags.set(config.flag, value); + } + } + + const full_flags = new Map(processed_flags); + + // process full flags + for (const [flag, config] of all) { + if (processed_flags.has(flag)) { + await config.validate?.(processed_flags.get(flag), processed_flags); + continue; + } + + if (config.required) { + throw new Error(config.required_error || `Missing required flag: ${flag}`); + } + + const val = await processMissingFlag(config, full_flags); + full_flags.set(flag, val); + } + + return { + specified: processed_flags, + full: full_flags, + }; +} diff --git a/release/utils/write.ts b/release/utils/write.ts new file mode 100644 index 00000000000..cc52f61566d --- /dev/null +++ b/release/utils/write.ts @@ -0,0 +1,3 @@ +export function write(content: string) { + console.log(content); +} diff --git a/scripts/-tarball-info.js b/scripts/-tarball-info.js deleted file mode 100644 index 8b76338f1df..00000000000 --- a/scripts/-tarball-info.js +++ /dev/null @@ -1,208 +0,0 @@ -'use strict'; -/* - This file generates meta information about what packages - are included in the project and the tarballs we would produce - for them were we to pack them using packages-for-commit. - - It exports that meta-information alongside a util helper - that can be used to insert the locations of our tarballs - into any package.json - - Example Package Meta: - - Given a project named `data` located in the folder `projects` - containing a package named `@ember-data/-example` - located at `projects/data/packages/-example` - with a `package.json` specifying a version of `3.0.0` - - e.g. - - /projects - /data - /packages - /-example - /package.json - - We would generate meta looking like the following: - - { - // the path to the directory for the package - location: "/path/to/projects/data/packages/-example", - - // the location of the package.json file for the package - fileLocation: "/path/to/projects/data/packages/some-package-directory/package.json", - - // the directory name of the package - localName: "-example", - - // the file location a generated tarball for this package would be placed - tarballLocation: "/path/to/projects/__tarball-cache/ember-data--example-3.0.0.tgz", - - // useful for making edits to add tarball paths to the contents - packageInfo: , - - // useful for restoring original state of a package.json after edits - originalPackageInfo: , - } - - We export this info for all packages from this module as `PackageInfos` - - Additionally, we export a util `insertTarballsToPackageJson(pathToPackageJson)` - - This util will discover any dependencies or devDependencies of the given - package.json that match the package names of the packges in PackageInfos - and rewrite the file replacing their version with the file path of the - tarball we would generate. - - E.g. - { - dependencies: { - "@ember-data/-example": "3.0.0" - } - } - - would become: - - { - dependencies: { - "@ember-data/-example": "file:/path/to/projects/__tarball-cache/ember-data--example-3.0.0.tgz" - } - } -*/ - -const fs = require('fs'); -const path = require('path'); -const url = require('url'); - -const execa = require('execa'); -const debug = require('debug')('tarball-info'); -const chalk = require('chalk'); -const cliArgs = require('command-line-args'); - -const projectRoot = path.resolve(__dirname, '../'); -// we share this for the build -const packagesDir = path.join(projectRoot, './packages'); -const packages = fs.readdirSync(packagesDir); -const OurPackages = {}; -const CurrentSha = execWithLog(`git rev-parse HEAD`); -const cacheDir = path.join(projectRoot, `../__tarball-cache`); -const tarballDir = path.join(cacheDir, CurrentSha); - -const optionsDefinitions = [ - { - name: 'hostPath', - alias: 'p', - type: String, - defaultValue: `file:${tarballDir}`, - }, - { - name: 'referenceViaVersion', - type: Boolean, - defaultValue: false, - }, -]; -const options = cliArgs(optionsDefinitions, { partial: true }); - -// ensure we add the trailing slash, otherwise `url.URL` will -// eliminate a directory by accident. -if (options.hostPath.charAt(options.hostPath.length - 1) !== '/') { - options.hostPath = options.hostPath + '/'; -} -if (options.hostPath.charAt(0) === '/') { - options.hostPath = 'file:' + options.hostPath; -} - -function execWithLog(command, force) { - debug(chalk.cyan('Executing: ') + chalk.yellow(command)); - if (debug.enabled || force) { - return execa.sync(command, { stdio: [0, 1, 2], shell: true }); - } - - return execa.sync(command, { shell: true }).stdout; -} - -function convertPackageNameToTarballName(str) { - str = str.replace('@', ''); - str = str.replace('/', '-'); - return str; -} - -packages.forEach((localName) => { - const pkgDir = path.join(packagesDir, localName); - const pkgPath = path.join(pkgDir, 'package.json'); - let pkgInfo; - try { - pkgInfo = require(pkgPath); - } catch (e) { - return; - } - if (pkgInfo.private === true) { - return; - } - const version = `${pkgInfo.version}-sha.${CurrentSha}`; - const tarballName = `${convertPackageNameToTarballName(pkgInfo.name)}-${version}.tgz`; - OurPackages[pkgInfo.name] = { - location: pkgDir, - fileLocation: pkgPath, - localName: localName, - version: version, - tarballName: tarballName, - localTarballLocation: path.join(tarballDir, tarballName), - reference: generatePackageReference(version, tarballName), - packageInfo: pkgInfo, - originalPackageInfo: fs.readFileSync(pkgPath), - }; -}); - -const AllPackages = Object.keys(OurPackages); - -function generatePackageReference(version, tarballName) { - if (options.referenceViaVersion === true) { - return version; - } - if (options.hostPath.indexOf('file:') === 0) { - return path.join(options.hostPath, tarballName); - } - return new url.URL(tarballName, options.hostPath); -} - -function insertTarballsToPackageJson(fileLocation, options = {}) { - // in some flows we have the potential to have previously written - // to the package.json already prior to calling this method. - // reading it in this way this ensures we get the latest and not - // a stale module from require - const location = require.resolve(fileLocation); - const pkgInfo = JSON.parse(fs.readFileSync(location, 'utf8')); - - if (options.isRelativeTarball) { - pkgInfo.version = `${pkgInfo.version}-sha.${CurrentSha}`; - } - - AllPackages.forEach((packageName) => { - const pkg = OurPackages[packageName]; - - if (pkgInfo.dependencies && pkgInfo.dependencies[packageName] !== undefined) { - pkgInfo.dependencies[packageName] = pkg.reference; - } else if (pkgInfo.devDependencies && pkgInfo.devDependencies[packageName] !== undefined) { - pkgInfo.devDependencies[packageName] = pkg.reference; - } - - if (!options.isRelativeTarball) { - const resolutions = (pkgInfo.resolutions = pkgInfo.resolutions || {}); - resolutions[packageName] = pkg.reference; - } - }); - - fs.writeFileSync(location, JSON.stringify(pkgInfo, null, 2)); -} - -module.exports = { - config: { - sha: CurrentSha, - cacheDir: cacheDir, - tarballDir: tarballDir, - options, - }, - PackageInfos: OurPackages, - insertTarballsToPackageJson, -}; diff --git a/scripts/asset-size-tracking/src/parse-modules.js b/scripts/asset-size-tracking/src/parse-modules.js index 31075544825..cac79f15091 100644 --- a/scripts/asset-size-tracking/src/parse-modules.js +++ b/scripts/asset-size-tracking/src/parse-modules.js @@ -1,6 +1,6 @@ const Library = require('./library'); -const moduleNames = ['ember-data', '@ember-data', '@ember/ordered-set', 'ember-inflector']; +const moduleNames = ['ember-data', '@ember-data', '@warp-drive']; module.exports = function parseModules(builtAsset) { let modules = builtAsset.split('define(').join('MODULE_SPLIT_POINTdefine(').split('MODULE_SPLIT_POINT'); @@ -23,13 +23,9 @@ module.exports = function parseModules(builtAsset) { let packageName = 'ember-data'; - if (name.indexOf('@ember-data/') === 0) { + if (name.indexOf('@ember-data/') === 0 || name.startsWith('@warp-drive/')) { let subPackageEnd = name.indexOf('/', 12); packageName = name.substring(0, subPackageEnd); - } else if (name.indexOf('ember-inflector') === 0) { - packageName = 'ember-inflector'; - } else if (name.indexOf('@ember/ordered-set') === 0) { - packageName = '@ember/ordered-set'; } library.getPackage(packageName).addModule(name, m); diff --git a/scripts/copy-declarations.mjs b/scripts/copy-declarations.mjs new file mode 100644 index 00000000000..94edd3cb403 --- /dev/null +++ b/scripts/copy-declarations.mjs @@ -0,0 +1,82 @@ +import chalk from 'chalk'; +import path from 'path'; +import { globby } from 'globby'; +import fs from 'fs'; + +/** @type {import('bun-types')} */ + +/** + * A small script to copy all `.d.ts` files from a `src` directory + * to a `dest` directory. + * + * This is useful because TypeScript doesn't include `.d.ts` files + * in the output directory when using `tsc` to compile. So any manually + * written `.d.ts` files wouldn't get copied into the published types + * directory. + * + * Input directory defaults to `src`. + * Output directory defaults to `unstable-preview-types`. + * + * Paths are relative to the current working directory from which + * the script is run. + * + * @example + * ```sh + * bun ../../scripts/copy-declarations.mjs addon dist-types + * ``` + */ +async function main() { + const args = Bun.argv.slice(2); + + if (args.length === 0) { + args.push('src', 'unstable-preview-types'); + } else if (args.length === 1) { + args.push('unstable-preview-types'); + } + + const [inputDir, outputDir] = args; + const inputPath = path.resolve(process.cwd(), inputDir); + const outputPath = path.resolve(process.cwd(), outputDir); + const relativeInputPath = path.relative(process.cwd(), inputPath); + const relativeOutputPath = path.relative(process.cwd(), outputPath); + + console.log( + chalk.grey( + chalk.bold( + `\nCopying ${chalk.cyan('**/*.d.ts')} files\n\tfrom: ${chalk.yellow(relativeInputPath)}\n\tto: ${chalk.yellow( + relativeOutputPath + )}` + ) + ) + ); + + const files = await globby([`${inputPath}/**/*.d.ts`]); + + if (files.length === 0) { + console.log(chalk.red(`\nNo **/*.d.ts files found in ${chalk.white(relativeInputPath)}\n`)); + process.exitCode = 1; + } + + console.log(chalk.grey(`\nFound ${chalk.cyan(files.length)} files\n`)); + + for (const file of files) { + const relativeFile = path.relative(process.cwd(), file); + const innerPath = path.relative(inputPath, file); + const outputFile = path.resolve(outputPath, innerPath); + const relativeOutFile = path.relative(process.cwd(), outputFile); + + console.log(chalk.grey(`\t${chalk.cyan(relativeFile)} => ${chalk.green(relativeOutFile)}`)); + + // ensure the output directory exists + const outDir = path.dirname(outputFile); + fs.mkdirSync(outDir, { recursive: true }); + + const inFile = Bun.file(file); + const outFile = Bun.file(outputFile); + await Bun.write(outFile, inFile); + } + + console.log(chalk.grey(chalk.bold(`\n✅ Copied ${chalk.cyan(files.length)} files\n`))); +} + +await main(); diff --git a/scripts/downlevel-addons.mjs b/scripts/downlevel-addons.mjs new file mode 100644 index 00000000000..54a506c27b6 --- /dev/null +++ b/scripts/downlevel-addons.mjs @@ -0,0 +1,157 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +function log(msg) { + console.log(chalk.grey(msg)); +} +function fixed(msg) { + log(`\t${chalk.blue('[FIXED]')} ${msg}`); +} + +function walkSync(dir, cb, pkgs = new Map()) { + const fullPath = path.join(process.cwd(), dir); + fs.readdirSync(fullPath).forEach((dirName) => { + const relativePath = path.join(dir, dirName); + const pkgPath = path.join(relativePath, 'package.json'); + const tsConfigPath = path.join(relativePath, 'tsconfig.json'); + const fullPkgPath = path.join(process.cwd(), pkgPath); + const fullTsConfigPath = path.join(process.cwd(), tsConfigPath); + + if (!fs.existsSync(fullPkgPath)) { + console.log(chalk.red(`🚨 Missing package.json in ${relativePath}`)); + return; + } + + const hasTsConfig = fs.existsSync(fullTsConfigPath); + const pkg = JSON.parse(fs.readFileSync(fullPkgPath, 'utf-8')); + const version = pkg.version; + const workspaceVersion = `workspace:${version}`; + + pkgs.set(pkg.name, { + dirName, + relativePath, + pkgPath, + tsConfigPath, + hasTsConfig, + fullPath: path.join(fullPath, dirName), + fullPkgPath, + fullTsConfigPath, + pkg, + name: pkg.name, + version, + workspaceVersion, + }); + }); + + pkgs.forEach((pkg) => cb(pkg, pkgs)); + return pkgs; +} + +function processPackage(info) { + if (!info.pkg['ember-addon']) { + return; + } + + log(`Validating ${chalk.yellow(info.name)}`); + const { fullPkgPath, pkg } = info; + let edited = false; + + if (info.pkg['ember-addon'].version === 2 && info.pkg['ember-addon'].preventDownleveling) { + // ensure that @warp-drive/build-config is in dependencies + if (!pkg.dependencies['@warp-drive/build-config']) { + fixed(`Added missing dependency @warp-drive/build-config`); + pkg.dependencies['@warp-drive/build-config'] = info.workspaceVersion; + edited = true; + } + + // ensure that @warp-drive/build-config is not in devDependencies + if (pkg.devDependencies['@warp-drive/build-config']) { + fixed(`Removed @warp-drive/build-config from devDependencies`); + delete pkg.devDependencies['@warp-drive/build-config']; + edited = true; + } + + // remove @embroider/addon-shim from dependencies + if (pkg.dependencies['@embroider/addon-shim']) { + fixed(`Removed @embroider/addon-shim from dependencies`); + delete pkg.dependencies['@embroider/addon-shim']; + edited = true; + } + + if (edited) { + fs.writeFileSync(fullPkgPath, JSON.stringify(pkg, null, 2) + '\n'); + } + + return; + } + + // ensure that we are v1 + if (info.pkg['ember-addon'].version !== 1) { + fixed(`Set ember-addon.version to 1`); + info.pkg['ember-addon'].version = 1; + edited = true; + } + + // ensure that @warp-drive/build-config is in dependencies + if (!pkg.dependencies['@warp-drive/build-config']) { + fixed(`Added missing dependency @warp-drive/build-config`); + pkg.dependencies['@warp-drive/build-config'] = info.workspaceVersion; + edited = true; + } + + // ensure that @warp-drive/build-config is not in devDependencies + if (pkg.devDependencies['@warp-drive/build-config']) { + fixed(`Removed @warp-drive/build-config from devDependencies`); + delete pkg.devDependencies['@warp-drive/build-config']; + edited = true; + } + + // remove @embroider/addon-shim from dependencies + if (pkg.dependencies['@embroider/addon-shim']) { + fixed(`Removed @embroider/addon-shim from dependencies`); + delete pkg.dependencies['@embroider/addon-shim']; + edited = true; + } + + // ensure that ember-auto-import is in dependencies if we are test infra + if (pkg.name === '@ember-data/unpublished-test-infra') { + if (!pkg.dependencies['ember-auto-import']) { + fixed(`Added missing dependency ember-auto-import`); + pkg.dependencies['ember-auto-import'] = '^2.7.2'; + edited = true; + } + } else { + // ensure that ember-auto-import is not in dependencies + if (pkg.dependencies['ember-auto-import']) { + fixed(`Removed ember-auto-import from dependencies`); + delete pkg.dependencies['ember-auto-import']; + edited = true; + } + } + + // ensure that ember-cli-babel is in dependencies + if (!pkg.dependencies['ember-cli-babel']) { + fixed(`Added missing dependency ember-cli-babel`); + pkg.dependencies['ember-cli-babel'] = '^8.2.0'; + edited = true; + } + + // ensure that ember-auto-import and ember-cli-babel are not in devDependencies + if (pkg.devDependencies['ember-auto-import']) { + fixed(`Removed ember-auto-import from devDependencies`); + delete pkg.devDependencies['ember-auto-import']; + edited = true; + } + if (pkg.devDependencies['ember-cli-babel']) { + fixed(`Removed ember-cli-babel from devDependencies`); + delete pkg.devDependencies['ember-cli-babel']; + edited = true; + } + + if (edited) { + fs.writeFileSync(fullPkgPath, JSON.stringify(pkg, null, 2) + '\n'); + } +} + +walkSync('packages', processPackage); diff --git a/scripts/explain.mjs b/scripts/explain.mjs new file mode 100644 index 00000000000..da9fa104d1d --- /dev/null +++ b/scripts/explain.mjs @@ -0,0 +1,45 @@ +import chalk from 'chalk'; + +/** @type {import('bun-types')} */ + +const MarkerLines = new Set(['devDependencies:', 'dependencies:', 'peerDependencies:']); +const GraphMarkers = new Set(['├', '│', '└', '─', '┬']); + +async function main() { + const args = Bun.argv.slice(2); + const pkgName = args[0]; + + console.log(chalk.grey(chalk.bold(`Explaining ${chalk.yellow(pkgName)} in ${chalk.yellow(process.cwd())}`))); + + const output = Bun.spawnSync(['pnpm', 'why', pkgName], { + cwd: process.cwd(), + env: process.env, + shell: true, + }); + + const versions = {}; + let currentSection = null; + + const logLines = output.stdout.toString().split('\n').filter(Boolean); + + for (const line of logLines) { + if (MarkerLines.has(line)) { + currentSection = line; + continue; + } + + if (currentSection) { + const sections = line.split(' '); + while (GraphMarkers.has(sections[0].charAt(0))) { + sections.shift(); + } + const [pkg, version, kind] = sections; + versions[pkg] = versions[pkg] ?? new Set(); + versions[pkg].add(version); + } + } + + console.log(versions); +} + +await main(); diff --git a/scripts/fix-dep-configuration.mjs b/scripts/fix-dep-configuration.mjs new file mode 100644 index 00000000000..e41ec18c98b --- /dev/null +++ b/scripts/fix-dep-configuration.mjs @@ -0,0 +1,198 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import JSONC from 'comment-json'; + +function log(msg) { + console.log(chalk.grey(msg)); +} +function fixed(msg) { + log(`\t${chalk.blue('[FIXED]')} ${msg}`); +} +function tsFixed(msg) { + log(`\t${chalk.green('[FIXED]')} ${msg}`); +} + +function getRelativePath(pkgA, pkgB) { + return path.relative(pkgA.fullPath, pkgB.fullPath); +} + +function walkSync(dir, cb, pkgs = new Map()) { + const fullPath = path.join(process.cwd(), dir); + fs.readdirSync(fullPath).forEach((dirName) => { + const relativePath = path.join(dir, dirName); + const pkgPath = path.join(relativePath, 'package.json'); + const tsConfigPath = path.join(relativePath, 'tsconfig.json'); + const fullPkgPath = path.join(process.cwd(), pkgPath); + const fullTsConfigPath = path.join(process.cwd(), tsConfigPath); + + if (!fs.existsSync(fullPkgPath)) { + console.log(chalk.red(`🚨 Missing package.json in ${relativePath}`)); + return; + } + + const hasTsConfig = fs.existsSync(fullTsConfigPath); + const pkg = JSON.parse(fs.readFileSync(fullPkgPath, 'utf-8')); + const version = pkg.version; + const workspaceVersion = `workspace:${version}`; + const tsConfig = hasTsConfig ? JSONC.parse(fs.readFileSync(fullTsConfigPath, 'utf-8')) : null; + + pkgs.set(pkg.name, { + dirName, + relativePath, + pkgPath, + tsConfigPath, + hasTsConfig, + fullPath: path.join(fullPath, dirName), + fullPkgPath, + fullTsConfigPath, + pkg, + tsConfig, + name: pkg.name, + version, + workspaceVersion, + }); + }); + + pkgs.forEach((pkg) => cb(pkg, pkgs)); + return pkgs; +} + +function hasReference(srcPkg, info) { + const referencePath = getRelativePath(srcPkg, info); + if (!srcPkg.tsConfig.references) { + return false; + } + return srcPkg.tsConfig.references.some((ref) => ref.path === referencePath); +} + +function hasPaths(srcPkg, info) { + if (!srcPkg.tsConfig.compilerOptions.paths) { + return false; + } + const dep = info.name; + const hasPrimary = !!srcPkg.tsConfig.compilerOptions.paths[dep]; + const hasWildcard = !!srcPkg.tsConfig.compilerOptions.paths[`${dep}/*`]; + + return hasPrimary && hasWildcard; +} + +function addPaths(srcPkg, info) { + const typesDir = info.tsConfig.compilerOptions?.declarationDir; + + if (!typesDir) { + throw new Error(`Missing compilerOptions.declarationDir in ${info.tsConfigPath}`); + } + + const relativePath = getRelativePath(srcPkg, info); + + srcPkg.tsConfig.compilerOptions.paths[info.name] = [`${relativePath}/${typesDir}`]; + srcPkg.tsConfig.compilerOptions.paths[`${info.name}/*`] = [`${relativePath}/${typesDir}/*`]; +} + +function processPackage(info, pkgs) { + log(`Validating ${chalk.yellow(info.name)}`); + const { fullPkgPath, fullTsConfigPath, pkg } = info; + let edited = false; + let tsConfigEdited = false; + /////////////////////////////////////////// + // ensure that peers are in devDependencies + /////////////////////////////////////////// + if (pkg.peerDependencies) { + if (!pkg.devDependencies) { + fixed(`Missing devDependencies hash`); + pkg.devDependencies = {}; + edited = true; + } + + for (const [peer, version] of Object.entries(pkg.peerDependencies)) { + if (!pkg.devDependencies[peer]) { + const addedVersion = pkgs.has(peer) ? pkgs.get(peer).workspaceVersion : version; + pkg.devDependencies[peer] = addedVersion; + edited = true; + fixed(`Added missing peer ${peer}@${version} to devDependencies @ ${chalk.magenta(addedVersion)}`); + } + } + } + const tsConfig = info.tsConfig; + + /////////////////////////////////////////////// + // ensure that all workspace deps are injected + /////////////////////////////////////////////// + const injected = new Set(); + Object.keys(pkg.dependencies ?? {}).forEach((dep) => { + if (pkgs.has(dep)) injected.add(dep); + }); + Object.keys(pkg.devDependencies ?? {}).forEach((dep) => { + if (pkgs.has(dep)) injected.add(dep); + }); + + if (injected.size > 0) { + if (!pkg.dependenciesMeta) { + fixed(`Added missing dependenciesMeta hash`); + pkg.dependenciesMeta = {}; + edited = true; + } + + for (const dep of injected) { + if (!pkg.dependenciesMeta[dep]) { + fixed(`Added missing injected: true for ${dep}`); + pkg.dependenciesMeta[dep] = { injected: true }; + edited = true; + } else if (!pkg.dependenciesMeta[dep].injected) { + fixed(`Set injected: true for ${dep}`); + pkg.dependenciesMeta[dep].injected = true; + edited = true; + } + + const relPkg = pkgs.get(dep); + + ///////////////////////////////////////////////////////////////////// + // ensure that the tsconfig.json has the correct paths and references + ///////////////////////////////////////////////////////////////////// + if (info.hasTsConfig && relPkg.hasTsConfig && relPkg.tsConfig.compilerOptions?.noEmit !== true) { + if (!tsConfig.references) { + tsConfig.references = []; + tsConfigEdited = true; + tsFixed(`Added references array to tsconfig.json`); + } + + if (!hasReference(info, relPkg)) { + const referencePath = getRelativePath(info, relPkg); + tsConfig.references.push({ path: referencePath }); + tsConfigEdited = true; + tsFixed(`Added reference to ${referencePath} in tsconfig.json`); + } + + if (!tsConfig.compilerOptions) { + tsConfig.compilerOptions = {}; + tsConfigEdited = true; + tsFixed(`Added compilerOptions hash to tsconfig.json`); + } + + if (!tsConfig.compilerOptions.paths) { + tsConfig.compilerOptions.paths = {}; + tsConfigEdited = true; + tsFixed(`Added paths hash to tsconfig.json`); + } + + if (!hasPaths(info, relPkg)) { + addPaths(info, relPkg); + tsConfigEdited = true; + tsFixed(`Added paths for ${dep} in tsconfig.json`); + } + } + } + } + + if (edited) { + fs.writeFileSync(fullPkgPath, JSON.stringify(pkg, null, 2) + '\n'); + } + if (tsConfigEdited) { + fs.writeFileSync(fullTsConfigPath, JSONC.stringify(tsConfig, null, 2) + '\n'); + } +} + +const pkgs = walkSync('packages', processPackage); + +walkSync('tests', processPackage, pkgs); diff --git a/scripts/generate-badge.mjs b/scripts/generate-badge.mjs new file mode 100644 index 00000000000..7484047a083 --- /dev/null +++ b/scripts/generate-badge.mjs @@ -0,0 +1,85 @@ +import { makeBadge } from 'badge-maker'; +import fs from 'fs'; +import path from 'path'; + +/* +{ + label: 'build', // (Optional) Badge label + message: 'passed', // (Required) Badge message + labelColor: '#555', // (Optional) Label color + color: '#4c1', // (Optional) Message color + logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iI2IxY2U1NiIvPjxwYXRoIGQ9Ik04IDBoMjR2NjRIOGMtNC40MzIgMC04LTMuNTY4LTgtOFY4YzAtNC40MzIgMy41NjgtOCA4LTh6IiBmaWxsPSIjNWQ1ZDVkIi8+PC9zdmc+' // (Optional) Any custom logo can be passed in a URL parameter by base64 encoding + links: ['https://example.com', 'https://example.com'], // (Optional) Links array of maximum two links + + // (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social' + // Each offers a different visual design. + style: 'flat', + + // (Optional) A string with only letters, numbers, -, and _. This can be used + // to ensure every element id within the SVG is unique and prevent CSS + // cross-contamination when the SVG badge is rendered inline in HTML pages. + idSuffix: 'dd' +} +*/ +const args = process.argv.slice(2); +const argNames = { + label: 'l', + message: 'm', + labelColor: 'lc', + color: 'c', + logoPath: 'p', + links: 'ls', + style: 's', + idSuffix: 'id', + out: 'o', +}; +const argsMap = { + l: 'label', + m: 'message', + lc: 'labelColor', + c: 'color', + p: 'logoPath', + ls: 'links', + s: 'style', + id: 'idSuffix', + o: 'out', +}; + +const format = { + color: 'grey', + style: 'flat', +}; +let outPath = ''; + +for (let i = 0; i < args.length; i += 2) { + if (!args[i].startsWith('-')) { + throw new Error(`Invalid argument: ${args[i]}`); + } + const arg = args[i].slice(1); + const value = args[i + 1]; + const name = arg in argNames ? arg : arg in argsMap ? argsMap[arg] : null; + + if (!name) { + throw new Error(`Invalid argument: ${arg}`); + } + + if (name !== 'logoPath' && name !== 'out') { + format[name] = value; + continue; + } + + if (name === 'out') { + outPath = path.join(process.cwd(), value); + continue; + } + + const base64 = fs.readFileSync(path.join(process.cwd(), value), 'utf8'); + format.logoBase64 = base64; // `data:image/svg+xml;base64,${base64}`; +} + +const svg = makeBadge(format); +if (outPath) { + fs.writeFileSync(outPath, svg); +} else { + console.log(svg); +} diff --git a/scripts/packages-for-commit.js b/scripts/packages-for-commit.js deleted file mode 100644 index c292266f1df..00000000000 --- a/scripts/packages-for-commit.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - This script generates tarballs for the current state of each package - in the project, placing them into a cache one level above the project. - - This is useful for being able to test a specific commit against another - project without publishing the commit to a registry. - - The tarballs produced will reference each other appropriately. For instance - if `@ember-data/store` has a dependency on `@ember-data/private-build-infra` the - tarball for `@ember-data/store` will have a package.json file whose version - of `@ember-data/private-build-infra` is the tarball for the commit for that package. -*/ - -'use strict'; - -const fs = require('fs'); - -const execa = require('execa'); -// apparently violates no-extraneous require? /shrug -const debug = require('debug')('test-external'); -const chalk = require('chalk'); - -// we share this for the build -const TarballConfig = require('./-tarball-info').config; -const OurPackages = require('./-tarball-info').PackageInfos; -const insertTarballsToPackageJson = require('./-tarball-info').insertTarballsToPackageJson; - -const tarballDir = TarballConfig.tarballDir; - -function execWithLog(command, force) { - debug(chalk.cyan('Executing: ') + chalk.yellow(command)); - if (debug.enabled || force) { - return execa.sync(command, { stdio: [0, 1, 2], shell: true }); - } - - return execa.sync(command, { shell: true }).stdout; -} - -if (!fs.existsSync(TarballConfig.cacheDir)) { - debug(`Ensuring Cache for Commit Builds at: ${TarballConfig.cacheDir}`); - fs.mkdirSync(TarballConfig.cacheDir); -} else { - debug(`Cache for Commit Builds Exists at: ${TarballConfig.cacheDir}`); -} - -if (!fs.existsSync(TarballConfig.tarballDir)) { - debug(`Ensuring Tarball Cache for SHA ${TarballConfig.sha} at: ${TarballConfig.tarballDir}`); - fs.mkdirSync(TarballConfig.tarballDir); -} else { - debug(`Tarball Cache Exists for SHA ${TarballConfig.sha} at: ${TarballConfig.tarballDir}`); -} - -const AllPackages = Object.keys(OurPackages); -const availablePackages = []; -AllPackages.forEach((packageName) => { - const pkg = OurPackages[packageName]; - - insertTarballsToPackageJson(pkg.fileLocation, { - fileDestination: pkg.tarballLocation, - isRelativeTarball: true, - }); - - execWithLog(` - cd ${tarballDir}; - npm pack ${pkg.location}; - `); - - availablePackages.push( - chalk.cyan(`"${pkg.packageInfo.name}"`) + chalk.white(': ') + chalk.grey(`"${pkg.reference}"`) - ); - - // cleanup - fs.writeFileSync(pkg.fileLocation, pkg.originalPackageInfo); -}); - -console.log( - chalk.cyan(`Successfully packaged commit ${chalk.white(TarballConfig.sha)}`) + - '\n\r\n\r' + - chalk.yellow(`The following packages have been generated:\n\r\t✅ `) + - chalk.grey(availablePackages.join('\n\r\t✅ ')) + - '\n\r\n\r' + - chalk.yellow(`The tarballs for these packages are available within ${chalk.white(tarballDir)}\n\r\n\r`) + - (!TarballConfig.options.referenceViaVersion && TarballConfig.options.hostPath.indexOf('file:') === 0 - ? chalk.red('⚠️ They may only be used on this machine.') - : chalk.yellow( - `⚠️ They can be hosted ${ - !TarballConfig.options.referenceViaVersion - ? 'only at ' + chalk.white(TarballConfig.options.hostPath) - : 'on any registry' - }` - )) -); diff --git a/scripts/publish.js b/scripts/publish.js deleted file mode 100644 index bad664fb507..00000000000 --- a/scripts/publish.js +++ /dev/null @@ -1,457 +0,0 @@ -'use strict'; - -/* -Usage - -publish lts|release|beta|canary|release--|lts-- - -Flags - ---distTag=latest|lts--|lts|beta|canary|release-- defaults to latest if channel is release, else defaults to channel ---version [optional] the exact version to tag these assets as ---fromVersion [optional] similar to version except treat this as the version to bump from ---bumpMajor ---bumpMinor ---skipVersion ---skipPack ---skipPublish ---skipSmokeTest ---dryRun - -Inspiration from https://github.com/glimmerjs/glimmer-vm/commit/01e68d7dddf28ac3200f183bffb7d520a3c71249#diff-19fef6f3236e72e3b5af7c884eef67a0 -*/ - -const fs = require('fs'); -const path = require('path'); -const readline = require('readline'); -const process = require('process'); - -const chalk = require('chalk'); -const execa = require('execa'); -const cliArgs = require('command-line-args'); -const semver = require('semver'); -const debug = require('debug')('publish-packages'); - -const projectRoot = path.resolve(__dirname, '../'); -const packagesDir = path.join(projectRoot, './packages'); -const packages = fs.readdirSync(packagesDir); -const testsDir = path.join(projectRoot, './tests'); -const tests = fs.readdirSync(testsDir); -const PreviousReleasePattern = /^release-(\d)-(\d+)$/; - -let isBugfixRelease = false; - -function cleanProject() { - execWithLog(`cd ${projectRoot} && rm -rf packages/*/dist packages/*/tmp packages/*/node_modules node_modules`); - execWithLog(`cd ${projectRoot} && pnpm install`); -} - -function scrubWorkspacesForHash(hash, newVersion) { - if (!hash) { - return; - } - Object.keys(hash).forEach(function (key) { - let val = hash[key]; - if (val.startsWith('workspace:')) { - hash[key] = `workspace:${newVersion}`; - } - }); -} -function scrubWorkspaces(pkg, path, newVersion) { - scrubWorkspacesForHash(pkg.dependencies, newVersion); - scrubWorkspacesForHash(pkg.peerDependencies, newVersion); - scrubWorkspacesForHash(pkg.devDependencies, newVersion); - fs.writeFileSync(path, JSON.stringify(pkg, null, 2), { encoding: 'utf8' }); -} - -/** - * - * @param {*} command The command to execute - * @param {*} proxyIO whether to proxy stdio from the main process for this command - * - * proxyIO=true is useful when you want to see the output log or respond to prompts - */ -function execWithLog(command, proxyIO = false) { - debug(chalk.cyan('Executing: ') + chalk.yellow(command)); - if (proxyIO) { - return execa.sync(command, { stdio: [0, 1, 2], shell: true, preferLocal: true }); - } - - return execa.sync(command, { shell: true, preferLocal: true }).stdout; -} - -function getConfig() { - const mainOptionsDefinitions = [{ name: 'channel', defaultOption: true }]; - const mainOptions = cliArgs(mainOptionsDefinitions, { stopAtFirstUnknown: true }); - const argv = mainOptions._unknown || []; - - if (!mainOptions.channel) { - throw new Error(`Incorrect usage of publish:\n\tpublish \n\nNo channel was specified`); - } - if (!['release', 'beta', 'canary', 'lts'].includes(mainOptions.channel)) { - const channel = mainOptions.channel; - let potentialRelease = !!channel && channel.match(PreviousReleasePattern); - if (potentialRelease && Array.isArray(potentialRelease)) { - isBugfixRelease = true; - } else { - throw new Error( - `Incorrect usage of publish:\n\tpublish \n\nChannel must be one of release|beta|canary|lts. Received ${mainOptions.channel}` - ); - } - } - - const optionsDefinitions = [ - { - name: 'distTag', - alias: 't', - type: String, - defaultValue: mainOptions.channel === 'release' ? 'latest' : mainOptions.channel, - }, - { - name: 'version', - alias: 'v', - type: String, - defaultValue: null, - }, - { - name: 'fromVersion', - type: String, - defaultValue: null, - }, - { name: 'skipVersion', type: Boolean, defaultValue: false }, - { name: 'skipPack', type: Boolean, defaultValue: false }, - { name: 'skipPublish', type: Boolean, defaultValue: false }, - { name: 'skipSmokeTest', type: Boolean, defaultValue: false }, - { name: 'bumpMajor', type: Boolean, defaultValue: false }, - { name: 'bumpMinor', type: Boolean, defaultValue: false }, - { name: 'force', type: Boolean, defaultValue: false }, - { name: 'dryRun', type: Boolean, defaultValue: false }, - ]; - const options = cliArgs(optionsDefinitions, { argv }); - const currentProjectVersion = options.fromVersion || require(path.join(__dirname, '../package.json')).version; - - if (isBugfixRelease && (options.bumpMajor || options.bumpMinor)) { - throw new Error(`Cannot bump major or minor version of a past release`); - } - - if (options.bumpMinor && options.bumpMajor) { - throw new Error(`Cannot bump both major and minor versions simultaneously`); - } - - options.channel = mainOptions.channel; - options.currentVersion = currentProjectVersion; - - return options; -} - -function assertGitIsClean(options) { - let status = execWithLog('git status'); - - if (!status.match(/^nothing to commit/m)) { - if (options.force) { - console.log( - chalk.white('⚠️ ⚠️ ⚠️ Local Git branch has uncommitted changes!\n\t') + - chalk.yellow('Passed option: ') + - chalk.white('--force') + - chalk.grey(' :: ignoring unclean git working tree') - ); - } else { - console.log( - chalk.red('💥 Git working tree is not clean. 💥 \n\t') + - chalk.grey('Use ') + - chalk.white('--force') + - chalk.grey(' to ignore this warning and publish anyway\n') + - chalk.yellow('⚠️ Publishing from an unclean working state may result in a broken release ⚠️\n\n') + - chalk.grey(`Status:\n${status}`) - ); - process.exit(1); - } - } - - if (!status.match(/^Your branch is up to date with/m)) { - if (options.force) { - console.log( - chalk.white('⚠️ ⚠️ ⚠️ Local Git branch is not in sync with origin branch') + - chalk.yellow('\n\tPassed option: ') + - chalk.white('--force') + - chalk.grey(' :: ignoring unsynced git branch') - ); - } else { - console.log( - chalk.red('💥 Local Git branch is not in sync with origin branch. 💥 \n\t') + - chalk.grey('Use ') + - chalk.white('--force') + - chalk.grey(' to ignore this warning and publish anyway\n') + - chalk.yellow('⚠️ Publishing from an unsynced working state may result in a broken release ⚠️') + - chalk.grey(`Status:\n${status}`) - ); - process.exit(1); - } - } - - let expectedChannelBranch = - options.distTag === 'canary' ? 'main' : options.distTag === 'latest' ? 'release' : options.distTag; - - if (options.channel === 'lts') { - expectedChannelBranch = `lts-${semver.major(options.currentVersion)}-${semver.minor(options.currentVersion)}`; - } - - let foundBranch = status.split('\n')[0]; - foundBranch = foundBranch.replace('On branch ', ''); - - if (foundBranch !== expectedChannelBranch) { - if (options.force) { - console.log( - chalk.white( - `⚠️ ⚠️ ⚠️ Expected to publish npm tag ${options.distTag} from the git branch ${expectedChannelBranch}, but found ${foundBranch}` - ) + - chalk.yellow('\n\tPassed option: ') + - chalk.white('--force') + - chalk.grey(' :: ignoring unexpected branch') - ); - } else { - console.log( - chalk.red( - `💥 Expected to publish npm tag ${options.distTag} from the git branch ${expectedChannelBranch}, but found ${foundBranch} 💥 \n\t` - ) + - chalk.grey('Use ') + - chalk.white('--force') + - chalk.grey(' to ignore this warning and publish anyway\n') + - chalk.yellow('⚠️ Publishing from an incorrect branch may result in a broken release ⚠️') - ); - process.exit(1); - } - } -} - -function retrieveNextVersion(options) { - /* - - A brief rundown of how version updates flow through the branches. - - - We only ever bump the major or minor version on main - - All other branches pick it up as those changes flow through the release cycle. - - See RELEASE.md for more about this - - #main 3.11.0-canary.x - releases with `canary` - #beta 3.10.0-beta.x - cuts from last 3.10.0-canary.x main with `beta` - #release 3.9.0 - cuts from last 3.9.0-beta.x - #lts 3.8.x - cuts from last 3.8.x on release -*/ - let v; - if (options.channel === 'release' || options.channel === 'lts') { - // a new patch, or our first release of a new minor/major - // usually for new minor/major the version will have drifted up - // from prior beta/canary incrementing - // bumpMajor means we are doing a re-release that makes us a new major release - // bumpMinor means we are doing a re-release that makes us a new minor release - // else this is a new patch release or the first release but cut from a previous beta. - let bumpType = options.bumpMajor ? 'major' : options.bumpMinor ? 'minor' : 'patch'; - v = semver.inc(options.currentVersion, bumpType); - } else if (options.channel === 'beta') { - // bumpMajor means we are doing a re-release that makes us the first beta of an upcoming major release - // bumpMinor means we are doing a re-release that makes us the first beta of an upcoming minor release - // else this is a new weekly beta or the first beta but cut from a previous canary. - let bumpType = options.bumpMajor ? 'premajor' : options.bumpMinor ? 'preminor' : 'prerelease'; - v = semver.inc(options.currentVersion, bumpType, 'beta'); - } else if (options.channel === 'canary') { - // bumpMajor is our first canary for an upcoming major - // bumpMinor is our first canary for an upcoming minor - // else this is a new nightly canary - let bumpType = options.bumpMajor ? 'premajor' : options.bumpMinor ? 'preminor' : 'prerelease'; - v = semver.inc(options.currentVersion, bumpType, 'alpha'); - } else if (isBugfixRelease) { - let bumpType = 'patch'; - v = semver.inc(options.currentVersion, bumpType); - } - - return v; -} - -function convertPackageNameToTarballName(str) { - str = str.replace('@', ''); - str = str.replace('/', '-'); - return str; -} - -function collectTarballPaths() { - const tarballs = []; - packages.forEach((localName) => { - const pkgDir = path.join(packagesDir, localName); - const pkgPath = path.join(pkgDir, 'package.json'); - const pkgInfo = require(pkgPath); - if (pkgInfo.private !== true) { - const tarballName = `${convertPackageNameToTarballName(pkgInfo.name)}-${pkgInfo.version}.tgz`; - tarballs.push(path.join(projectRoot, tarballName)); - } - }); - return tarballs; -} - -function bumpAllPackages(nextVersion) { - function bump(baseDir, localName) { - const pkgDir = path.join(baseDir, localName); - const pkgPath = path.join(pkgDir, 'package.json'); - const pkgInfo = require(pkgPath); - pkgInfo.version = nextVersion; - scrubWorkspaces(pkgInfo, pkgPath, nextVersion); - } - packages.forEach((l) => bump(packagesDir, l)); - tests.forEach((l) => bump(testsDir, l)); - const pkgJsonPath = path.join(projectRoot, './package.json'); - const pkgInfo = require(pkgJsonPath); - pkgInfo.version = nextVersion; - scrubWorkspaces(pkgInfo, pkgJsonPath, nextVersion); -} - -function packAllPackages() { - packages.forEach((localName) => { - const pkgDir = path.join(packagesDir, localName); - const pkgPath = path.join(pkgDir, 'package.json'); - const pkgInfo = require(pkgPath); - if (pkgInfo.private !== true) { - // will pack into the project root directory - // due to an issue where npm does not run prepublishOnly for pack, we run it here - // however this is also a timing bug, as typically it would be run *after* prepublish - // and prepare and now it is run *before* - // we do not use `prepublish` or `prepare` so this should be fine for now. - // https://docs.npmjs.com/misc/scripts - // https://github.com/npm/npm/issues/15363 - if (pkgInfo.scripts) { - if (pkgInfo.scripts.prepack) { - execWithLog(`cd ${pkgDir} && pnpm run prepack`); - } - } - execWithLog(`cd ${pkgDir} && pnpm pack --pack-destination=${projectRoot}`); - } - }); -} - -async function getOTPToken() { - let token = await question(chalk.green('\nPlease provide OTP token ')); - - return token.trim(); -} -function question(prompt) { - return new Promise((resolve) => { - cli.question(prompt, resolve); - }); -} -let cli = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); - -/** - * If otp is passed add it as a parameter to the publish command else assume authentication is setup either - * as environment variable - * - * @param {string} distTag - Use this tag on npm for this instance - * @param {string} tarball - Path to the tarball - * @param {string} otp - Token to make publish requests to npm - */ -function publishPackage(distTag, tarball, otp) { - let cmd = `npm publish ${tarball} --tag=${distTag} --access=public`; - - if (otp) { - cmd += ` --otp=${otp}`; - } - - execWithLog(cmd); -} - -async function confirmPublish(tarballs, options, promptOtp = true) { - let otp; - - if (promptOtp && !options.dryRun) { - otp = await getOTPToken(); - } - - for (let tarball of tarballs) { - if (options.dryRun) { - console.log('Would have published', tarball, 'with tag', options.distTag); - } else { - try { - publishPackage(options.distTag, tarball, otp); - } catch (e) { - // the token is outdated, we need another one - if (e.message.includes('E401') || e.message.includes('EOTP')) { - otp = await getOTPToken(); - - publishPackage(options.distTag, tarball, otp); - } else { - throw e; - } - } - } - } -} - -async function main() { - const options = getConfig(); - - assertGitIsClean(options); - - if (!options.skipSmokeTest) { - execWithLog(`pnpm run lint:js && pnpm run test`, debug.enabled); - console.log(`✅ ` + chalk.cyan(`Project passes Smoke Test`)); - } else { - console.log(`⚠️ ` + chalk.grey(`Skipping Smoke Test`)); - } - - let nextVersion = options.currentVersion; - if (!options.skipVersion) { - nextVersion = options.version || retrieveNextVersion(options); - bumpAllPackages(nextVersion); - let commitCommand = `git commit -am "Release v${nextVersion}"`; - if (!options.dryRun) { - commitCommand = `pnpm install --no-frozen-lockfile && ` + commitCommand; - commitCommand += ` && git tag v${nextVersion}`; - } - - // Let the github action determine whether to push the tag to remote - if (!process.env.CI) { - commitCommand += ` && git push && git push origin v${nextVersion}`; - } - - execWithLog(commitCommand, true); - console.log(`✅ ` + chalk.cyan(`Successfully Versioned ${nextVersion}`)); - } else { - console.log('⚠️ ' + chalk.grey(`Skipping Versioning`)); - } - - if (!options.skipPack) { - cleanProject(); - packAllPackages(); - console.log(`✅ ` + chalk.cyan(`Successfully Packaged ${nextVersion}`)); - } else { - console.log('⚠️ ' + chalk.grey(`Skipping Packaging`)); - } - - if (!options.skipPublish) { - const tarballs = collectTarballPaths(); - const npmAuthTokenInEnv = !!process.env.NODE_AUTH_TOKEN; - if (!npmAuthTokenInEnv && !options.dryRun) { - if (process.env.CI) { - throw new Error('No NODE_AUTH_TOKEN environment variable, cannot continue publishing.'); - } - } - // Assume human ran script if token is missing - await confirmPublish(tarballs, options, !npmAuthTokenInEnv); - console.log(`✅ ` + chalk.cyan(`Successfully Published ${nextVersion}`)); - } else { - console.log('⚠️ ' + chalk.grey(`Skipping Publishing`)); - } -} - -main() - .finally(() => cli.close()) - .catch((reason) => { - console.error(reason); - process.exit(1); - }); diff --git a/scripts/test-external-partner-project.js b/scripts/test-external-partner-project.js deleted file mode 100644 index f97506ba86e..00000000000 --- a/scripts/test-external-partner-project.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const execa = require('execa'); -// apparently violates no-extraneous require? /shrug -const debug = require('debug')('test-external'); -const rimraf = require('rimraf'); -const chalk = require('chalk'); -const cliArgs = require('command-line-args'); - -const projectRoot = path.resolve(__dirname, '../'); - -let cliOptionsDef = [{ name: 'projectName', defaultOption: true }]; -let cliOptions = cliArgs(cliOptionsDef, { stopAtFirstUnknown: true }); -const externalProjectName = cliOptions.projectName; -let argv = cliOptions._unknown || []; -cliOptionsDef = [{ name: 'gitUrl', defaultOption: true }]; -cliOptions = cliArgs(cliOptionsDef, { stopAtFirstUnknown: true, argv }); -const gitUrl = cliOptions.gitUrl; -argv = cliOptions._unknown || []; -cliOptionsDef = [ - { name: 'skipSmokeTest', type: Boolean, defaultValue: false }, - { name: 'skipClone', type: Boolean, defaultValue: false }, - { name: 'skipTest', type: Boolean, defaultValue: false }, - { name: 'noLockFile', type: Boolean, defaultValue: false }, - { name: 'useCache', type: Boolean, defaultValue: false }, -]; -cliOptions = cliArgs(cliOptionsDef, { argv }); - -const { skipSmokeTest, skipClone, skipTest, noLockFile, useCache } = cliOptions; - -// we share this for the build -const cachePath = '../__external-test-cache'; -const tempDir = path.join(projectRoot, cachePath); -const projectTempDir = path.join(tempDir, externalProjectName); -const insertTarballsToPackageJson = require('./-tarball-info').insertTarballsToPackageJson; - -if (!gitUrl) { - throw new Error('No git url provided to `test-external-partner`. An https git url should be the first argument.'); -} else if (gitUrl.indexOf('https') !== 0) { - throw new Error(`The git url provided to \`node test-external-partner\` should use https. Received '${gitUrl}'`); -} - -console.log( - `Preparing to test external project ${externalProjectName} located at ${gitUrl} against this ember-data commit.` -); - -function execExternal(command, force) { - command = `cd ${projectTempDir} && ${command}`; - return execWithLog(command, force); -} - -function execWithLog(command, force) { - debug(chalk.cyan('Executing: ') + chalk.yellow(command)); - if (debug.enabled || force) { - return execa.sync(command, { stdio: [0, 1, 2], shell: true }); - } - - return execa.sync(command, { shell: true }).stdout; -} - -if (!fs.existsSync(tempDir)) { - debug(`Ensuring Cache Root at: ${tempDir}`); - fs.mkdirSync(tempDir); -} else { - debug(`Cache Root Exists at: ${tempDir}`); -} - -if (fs.existsSync(projectTempDir)) { - if (!skipClone) { - debug(`Cleaning Cache at: ${projectTempDir}`); - rimraf.sync(projectTempDir); - } else { - debug(`Skipping Cache Clean at: ${projectTempDir}`); - } -} else { - debug(`No pre-existing cache present at: ${projectTempDir}`); -} - -// install the project -try { - if (!skipClone) { - execWithLog(`git clone --depth=1 ${gitUrl} ${projectTempDir}`); - } else { - debug(`Skipping git clone`); - } -} catch (e) { - debug(e); - throw new Error( - `Install of ${gitUrl} in ${projectTempDir} for external project ${externalProjectName} testing failed.` - ); -} - -const usePnpm = fs.existsSync(path.join(projectTempDir, 'yarn.lock')); -const packageJsonLocation = path.join(projectTempDir, 'package.json'); - -// run project tests -console.log(`Running tests for ${externalProjectName}`); - -let smokeTestPassed = true; -let commitTestPassed = true; - -try { - if (skipSmokeTest) { - debug('Skipping Smoke Test'); - } else { - debug('Running Smoke Test'); - try { - execExternal(`${usePnpm ? 'pnpm install' : 'npm install'}`); - } catch (e) { - debug(e); - throw new Error(`Unable to complete install of dependencies for external project ${externalProjectName}`); - } - execExternal(`ember test`, true); - } -} catch (e) { - smokeTestPassed = false; -} - -try { - debug('Preparing Package To Run Tests Against Commit'); - insertTarballsToPackageJson(packageJsonLocation); - - // clear node_modules installed for the smoke-test - execExternal(`rm -rf node_modules`); - // we are forced to use pnpm so that our resolutions will be respected - // in addition to the version file link we insert otherwise nested deps - // may bring their own ember-data - // - // For this reason we don't trust the lock file - // we also can't trust the cache - execExternal(`pnpm install${noLockFile ? ' --no-lockfile' : ''}${useCache ? '' : ' --cache-folder=tmp/yarn-cache'}`); -} catch (e) { - console.log(`Unable to npm install tarballs for ember-data\` for ${externalProjectName}. Original error below:`); - - throw e; -} - -if (!skipTest) { - try { - debug('Running tests against EmberData commit'); - execExternal(`ember --version`, true); - // ember-cli test command does not have --output-path available - // in all versions of our partner test's - execExternal(`ember build`, true); - execExternal(`ember test --path="./dist"`, true); - } catch (e) { - commitTestPassed = false; - } -} - -if (skipTest) { - console.log(`Skipped Tests: Commit viability unknown`); -} else { - if (skipSmokeTest && !commitTestPassed) { - throw new Error('Commit may result in a regression, but the smoke test was skipped.'); - } else if (!smokeTestPassed && !commitTestPassed) { - throw new Error(`Commit may result in a regression, but the smoke test for ${externalProjectName} also failed.`); - } else if (smokeTestPassed && !commitTestPassed) { - throw new Error(`Commit results in a regression in ${externalProjectName}`); - } else if (!smokeTestPassed) { - console.log(`Commit may resolve issues present in the smoke test for ${externalProjectName}`); - } else { - console.log(`Commit does not regress ${externalProjectName}`); - } -} diff --git a/scripts/uplevel-adons.mjs b/scripts/uplevel-adons.mjs new file mode 100644 index 00000000000..6c2cb6e3c22 --- /dev/null +++ b/scripts/uplevel-adons.mjs @@ -0,0 +1,76 @@ +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +function log(msg) { + console.log(chalk.grey(msg)); +} +function fixed(msg) { + log(`\t${chalk.blue('[FIXED]')} ${msg}`); +} + +function walkSync(dir, cb, pkgs = new Map()) { + const fullPath = path.join(process.cwd(), dir); + fs.readdirSync(fullPath).forEach((dirName) => { + const relativePath = path.join(dir, dirName); + const pkgPath = path.join(relativePath, 'package.json'); + const tsConfigPath = path.join(relativePath, 'tsconfig.json'); + const fullPkgPath = path.join(process.cwd(), pkgPath); + const fullTsConfigPath = path.join(process.cwd(), tsConfigPath); + + if (!fs.existsSync(fullPkgPath)) { + console.log(chalk.red(`🚨 Missing package.json in ${relativePath}`)); + return; + } + + const hasTsConfig = fs.existsSync(fullTsConfigPath); + const pkg = JSON.parse(fs.readFileSync(fullPkgPath, 'utf-8')); + const version = pkg.version; + const workspaceVersion = `workspace:${version}`; + + pkgs.set(pkg.name, { + dirName, + relativePath, + pkgPath, + tsConfigPath, + hasTsConfig, + fullPath: path.join(fullPath, dirName), + fullPkgPath, + fullTsConfigPath, + pkg, + name: pkg.name, + version, + workspaceVersion, + }); + }); + + pkgs.forEach((pkg) => cb(pkg, pkgs)); + return pkgs; +} + +function processPackage(info) { + if (!info.pkg['ember-addon'] || info.pkg['ember-addon'].version === 2) { + return; + } + + log(`Validating ${chalk.yellow(info.name)}`); + const { fullPkgPath, pkg } = info; + + // ensure that we are v2 + fixed(`Set ember-addon.version to 2`); + info.pkg['ember-addon'].version = 2; + + // ensure that ember-auto-import and ember-cli-babel are not in dependencies + if (pkg.dependencies['ember-auto-import']) { + fixed(`Removed dependency ember-auto-import`); + delete pkg.dependencies['ember-auto-import']; + } + if (pkg.dependencies['ember-cli-babel']) { + fixed(`Removed dependency ember-cli-babel`); + delete pkg.dependencies['ember-cli-babel']; + } + + fs.writeFileSync(fullPkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} + +walkSync('packages', processPackage); diff --git a/scripts/validate-deps.js b/scripts/validate-deps.js new file mode 100644 index 00000000000..921bb4a3688 --- /dev/null +++ b/scripts/validate-deps.js @@ -0,0 +1,275 @@ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const root = process.cwd(); + +const pkgs = new Map(); +const otherPkgs = new Set([]); +const files = new Map(); +const currentVersion = require(path.join(root, 'package.json')).version; +const peer_exceptions = { + '@ember-data/active-record': { + '@ember-data/store': true, + }, + '@ember-data/rest': { + '@ember-data/store': true, + }, +}; +const ignore_hardlinks = new Set(['@warp-drive/internal-config']); + +function isPeerException(pkg, dep) { + return Boolean(peer_exceptions[pkg] && peer_exceptions[pkg][dep]); +} + +function getRequiredPeers(dep, version = '*', seen = new Map()) { + const pkg = pkgs.get(dep); + if (!pkg) { + if (otherPkgs.has(dep)) { + seen.set(dep, version); + } + + // TODO - handle otherPkgs that aren't these + return seen; + } + seen.set(dep, version); + + if (pkg.peerDependencies) { + Object.entries(pkg.peerDependencies).forEach(([peer, version]) => { + getRequiredPeers(peer, version, seen); + }); + } + + return seen; +} + +fs.readdirSync(path.join(root, 'packages')).forEach((dirName) => { + const pkg = require(path.join(root, 'packages', dirName, 'package.json')); + pkgs.set(pkg.name, pkg); + files.set(pkg.name, { + path: path.join(root, 'packages', dirName, 'package.json'), + pkg, + }); +}); + +fs.readdirSync(path.join(root, 'tests')).forEach((dirName) => { + const pkg = require(path.join(root, 'tests', dirName, 'package.json')); + pkgs.set(pkg.name, pkg); + files.set(pkg.name, { + path: path.join(root, 'tests', dirName, 'package.json'), + pkg, + }); +}); + +const configPkg = require(path.join(root, './config/package.json')); +pkgs.set(configPkg.name, configPkg); +files.set(configPkg.name, { + path: path.join(root, './config/package.json'), + configPkg, +}); + +pkgs.forEach((pkg) => { + let edited = false; + console.log( + chalk.grey(`\tValidating ${pkg.private ? '(private) ' : ''}${chalk.yellow(pkg.name)}@${chalk.magenta(pkg.version)}`) + ); + + if (!pkg.scripts) { + console.log(chalk.grey(`\t\t[FIX] Missing scripts`)); + edited = true; + pkg.scripts = {}; + } + // if (!pkg.scripts['_syncPnpm']) { + // console.log(`Missing _syncPnpm script for ${pkg.name}`); + // edited = true; + // pkg.scripts['_syncPnpm'] = 'bun run sync-dependencies-meta-injected'; + // } + if (pkg.scripts['prepare']) { + console.log(chalk.grey(`\t\t[FIX] Removing scripts.prepare`)); + edited = true; + delete pkg.scripts['prepare']; + } + + Object.entries(pkg.dependencies ?? {}).forEach(([dep, version]) => { + if (pkgs.has(dep)) { + const depVersion = pkgs.get(dep).version; + const wsVersion = `workspace:${depVersion}`; + + if (version !== wsVersion) { + console.log(`Dependency mismatch for ${pkg.name} -> ${dep} - expected ${wsVersion} but found ${version}`); + edited = true; + pkg.dependencies[dep] = wsVersion; + } + } + + if (pkgs.has(dep) || otherPkgs.has(dep)) { + if (ignore_hardlinks.has(dep)) { + if (pkg.dependenciesMeta?.[dep]?.injected) { + console.log(`Removing hardlink for ${pkg.name}`); + edited = true; + if (Object.keys(pkg.dependenciesMeta[dep]).length === 1) { + delete pkg.dependenciesMeta[dep]; + } else { + delete pkg.dependenciesMeta[dep].injected; + } + } + return; + } + if (!pkg.dependenciesMeta) { + console.log(`Missing dependenciesMeta for ${pkg.name}`); + edited = true; + pkg.dependenciesMeta = {}; + } + if (!pkg.dependenciesMeta[dep]) { + console.log(`Missing dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep] = {}; + } + if (!pkg.dependenciesMeta[dep].injected) { + console.log(`Missing injected: true in dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep].injected = true; + } + } + }); + + Object.entries(pkg.peerDependencies ?? {}).forEach(([dep, version]) => { + if (pkgs.has(dep)) { + const depVersion = pkgs.get(dep).version; + const wsVersion = `workspace:${depVersion}`; + + if (version !== wsVersion && !isPeerException(pkg.name, dep)) { + console.log(`Peer Dependency mismatch for ${pkg.name} -> ${dep} - expected ${wsVersion} but found ${version}`); + edited = true; + pkg.peerDependencies[dep] = wsVersion; + } + + const requiredPeers = getRequiredPeers(dep); + requiredPeers.delete(dep); + requiredPeers.forEach((version, peer) => { + if (!pkg.devDependencies || !pkg.devDependencies[peer]) { + console.log(`\tMissing transient peer dependency ${peer}@${version} for ${pkg.name} -> ${dep}`); + edited = true; + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + pkg.devDependencies[peer] = pkgs.has(peer) ? `workspace:${pkgs.get(peer).version}` : version; + } + }); + } + + if (pkgs.has(dep) || otherPkgs.has(dep)) { + if (ignore_hardlinks.has(dep)) { + if (pkg.dependenciesMeta?.[dep]?.injected) { + console.log(`Removing hardlink for ${pkg.name}`); + edited = true; + if (Object.keys(pkg.dependenciesMeta[dep]).length === 1) { + delete pkg.dependenciesMeta[dep]; + } else { + delete pkg.dependenciesMeta[dep].injected; + } + } + return; + } + + if (!pkg.devDependencies) { + console.log(`Missing devDependencies for ${pkg.name}`); + edited = true; + pkg.devDependencies = {}; + } + if (!pkg.devDependencies[dep]) { + console.log(`Missing devDependencies for ${pkg.name} -> ${dep}`); + edited = true; + pkg.devDependencies[dep] = otherPkgs.has(dep) ? version : `workspace:${pkgs.get(dep).version}`; + } + if (!pkg.dependenciesMeta) { + console.log(`Missing (dev) dependenciesMeta for ${pkg.name}`); + edited = true; + pkg.dependenciesMeta = {}; + } + if (!pkg.dependenciesMeta[dep]) { + console.log(`Missing (dev) dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep] = {}; + } + if (!pkg.dependenciesMeta[dep].injected) { + console.log(`Missing injected: true in (dev) dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep].injected = true; + } + } + }); + + const deps = Object.entries(pkg.devDependencies ?? {}); + + for (let i = 0; i < deps.length; i++) { + const [dep, version] = deps[i]; + + if (pkgs.has(dep)) { + const depVersion = pkgs.get(dep).version; + const wsVersion = `workspace:${depVersion}`; + + if (version !== wsVersion && !isPeerException(pkg.name, dep)) { + console.log(`Dev Dependency mismatch for ${pkg.name} -> ${dep} - expected ${wsVersion} but found ${version}`); + edited = true; + pkg.devDependencies[dep] = wsVersion; + } + + const requiredPeers = getRequiredPeers(dep); + requiredPeers.delete(dep); + requiredPeers.forEach((version, peer) => { + if (!pkg.devDependencies[peer]) { + console.log(`\tMissing transient peer dependency ${peer}@${version} for ${pkg.name} -> ${dep}`); + edited = true; + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + pkg.devDependencies[peer] = pkgs.has(peer) ? `workspace:${pkgs.get(peer).version}` : version; + deps.push([peer, version]); + } + }); + } + + if (pkgs.has(dep) || otherPkgs.has(dep)) { + if (ignore_hardlinks.has(dep)) { + if (pkg.dependenciesMeta?.[dep]?.injected) { + console.log(`Removing hardlink for ${pkg.name}`); + edited = true; + if (Object.keys(pkg.dependenciesMeta[dep]).length === 1) { + delete pkg.dependenciesMeta[dep]; + } else { + delete pkg.dependenciesMeta[dep].injected; + } + } + continue; + } + + if (!pkg.dependenciesMeta) { + console.log(`Missing (dev) dependenciesMeta for ${pkg.name}`); + edited = true; + pkg.dependenciesMeta = {}; + } + if (!pkg.dependenciesMeta[dep]) { + console.log(`Missing (dev) dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep] = {}; + } + if (!pkg.dependenciesMeta[dep].injected) { + console.log(`Missing injected: true in (dev) dependenciesMeta for ${pkg.name} -> ${dep}`); + edited = true; + pkg.dependenciesMeta[dep].injected = true; + } + } + } + + if (pkg.devDependenciesMeta) { + console.log(`Merging devDependenciesMeta into dependenciesMeta for ${pkg.name}`); + edited = true; + pkg.dependenciesMeta = pkg.dependenciesMeta ?? {}; + Object.assign(pkg.dependenciesMeta, pkg.devDependenciesMeta); + delete pkg.devDependenciesMeta; + } + + if (edited) { + fs.writeFileSync(files.get(pkg.name).path, JSON.stringify(pkg, null, 2) + '\n'); + } +}); diff --git a/tests/adapter-encapsulation/.ember-cli b/tests/adapter-encapsulation/.ember-cli deleted file mode 100644 index ee64cfed2a8..00000000000 --- a/tests/adapter-encapsulation/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/tests/adapter-encapsulation/.gitignore b/tests/adapter-encapsulation/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/adapter-encapsulation/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/adapter-encapsulation/.watchmanconfig b/tests/adapter-encapsulation/.watchmanconfig deleted file mode 100644 index e7834e3e4f3..00000000000 --- a/tests/adapter-encapsulation/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/tests/adapter-encapsulation/README.md b/tests/adapter-encapsulation/README.md deleted file mode 100644 index 450c917cc8a..00000000000 --- a/tests/adapter-encapsulation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# encapsulation-test-app - -This README outlines the details of collaborating on this Ember application. -A short introduction of this app could easily go here. - -## Prerequisites - -You will need the following things properly installed on your computer. - -* [Git](https://git-scm.com/) -* [Node.js](https://nodejs.org/) (with npm) -* [Ember CLI](https://ember-cli.com/) -* [Google Chrome](https://google.com/chrome/) - -## Installation - -* `git clone ` this repository -* `cd encapsulation-test-app` -* `npm install` - -## Running / Development - -* `ember serve` -* Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). - -### Code Generators - -Make use of the many generators for code, try `ember help generate` for more details - -### Running Tests - -* `ember test` -* `ember test --server` - -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - -### Building - -* `ember build` (development) -* `ember build --environment production` (production) - -### Deploying - -Specify what it takes to deploy your app. - -## Further Reading / Useful Links - -* [ember.js](https://emberjs.com/) -* [ember-cli](https://ember-cli.com/) -* Development Browser Extensions - * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) - * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/adapter-encapsulation/app/index.html b/tests/adapter-encapsulation/app/index.html deleted file mode 100644 index 51a527e7175..00000000000 --- a/tests/adapter-encapsulation/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EncapsulationTestApp - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/adapter-encapsulation/app/services/store.js b/tests/adapter-encapsulation/app/services/store.js deleted file mode 100644 index c8063c361fd..00000000000 --- a/tests/adapter-encapsulation/app/services/store.js +++ /dev/null @@ -1,16 +0,0 @@ -import Cache from '@ember-data/json-api'; -import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; -import RequestManager from '@ember-data/request'; -import Store, { CacheHandler } from '@ember-data/store'; - -export default class DefaultStore extends Store { - constructor() { - super(...arguments); - this.requestManager = new RequestManager(); - this.requestManager.use([LegacyNetworkHandler]); - this.requestManager.useCache(CacheHandler); - } - createCache(storeWrapper) { - return new Cache(storeWrapper); - } -} diff --git a/tests/adapter-encapsulation/app/templates/application.hbs b/tests/adapter-encapsulation/app/templates/application.hbs deleted file mode 100644 index ebe6a496046..00000000000 --- a/tests/adapter-encapsulation/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
- {{outlet}} -
\ No newline at end of file diff --git a/tests/adapter-encapsulation/config/environment.js b/tests/adapter-encapsulation/config/environment.js deleted file mode 100644 index 589ffba0e1d..00000000000 --- a/tests/adapter-encapsulation/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'adapter-encapsulation-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/adapter-encapsulation/ember-cli-build.js b/tests/adapter-encapsulation/ember-cli-build.js deleted file mode 100644 index 8f86cbd8ec6..00000000000 --- a/tests/adapter-encapsulation/ember-cli-build.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/tests/adapter-encapsulation/package.json b/tests/adapter-encapsulation/package.json deleted file mode 100644 index c78d4619c5c..00000000000 --- a/tests/adapter-encapsulation/package.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "name": "adapter-encapsulation-test-app", - "version": "4.12.8", - "private": true, - "description": "Small description for encapsulation-test-app goes here", - "repository": { - "type": "git", - "url": "https://github.com/emberjs/data.git", - "directory": "tests/adapter-encapsulation" - }, - "license": "MIT", - "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", - "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/debug": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "dependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/debug": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^3.0.1 || ^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "webpack": "^5.77.0" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "ember": { - "edition": "octane" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/adapter-encapsulation/testem.js b/tests/adapter-encapsulation/testem.js deleted file mode 100644 index e10b064501a..00000000000 --- a/tests/adapter-encapsulation/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - }, - Firefox: { - ci: ['-headless', '-width 1440', '-height 900'], - }, -}; diff --git a/tests/adapter-encapsulation/tests/index.html b/tests/adapter-encapsulation/tests/index.html deleted file mode 100644 index 65eb49adaae..00000000000 --- a/tests/adapter-encapsulation/tests/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - EncapsulationTestApp Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - - {{content-for "body-footer"}} - - - diff --git a/tests/adapter-encapsulation/tests/integration/belongs-to-test.js b/tests/adapter-encapsulation/tests/integration/belongs-to-test.js deleted file mode 100644 index 64c0bfcf90e..00000000000 --- a/tests/adapter-encapsulation/tests/integration/belongs-to-test.js +++ /dev/null @@ -1,530 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; -import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - let json = { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - relationships: {}, - }, - }; - - snapshot.eachRelationship((key, relationship) => { - if (relationship.kind === 'belongsTo') { - this.serializeBelongsTo(snapshot, json, relationship); - } else if (relationship.kind === 'hasMany') { - this.serializeHasMany(snapshot, json, relationship); - } - }); - - if (Object.keys(json.data.relationships).length === 0) { - delete json.data.relationships; - } - - return json; - } - - // minimal implementation, not json-api compliant - serializeBelongsTo(snapshot, json, relationship) { - let key = relationship.name; - let belongsTo = snapshot.belongsTo(key); - - if (belongsTo) { - let value = { - data: { - id: belongsTo.id, - type: belongsTo.modelName, - }, - }; - json.data.relationships[key] = value; - } - } - - // minimal implementation, not json-api compliant - serializeHasMany(snapshot, json, relationship) { - let key = relationship.key; - let hasMany = snapshot.hasMany(key); - - if (hasMany && hasMany.length) { - let value = { - data: hasMany.map((snap) => ({ - id: snap.id, - type: snap.modelName, - })), - }; - json.data.relationships[key] = value; - } - } -} - -class Post extends Model { - @attr - text; - - @hasMany('comments', { async: true, inverse: 'post' }) - comments; -} - -class Comment extends Model { - @attr - text; - - @belongsTo('post', { async: true, inverse: 'comments' }) - post; -} - -module('integration/belongs-to - Belongs To Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:post', Post); - this.owner.register('model:comment', Comment); - }); - - test('if a belongsTo relationship has a link but no data (findBelongsTo is defined)', async function (assert) { - let findRecordCalled = 0; - let findBelongsToCalled = 0; - - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - links: { - related: 'https://example.com/api/post/2', - }, - }, - }, - }, - }; - - let expectedResult = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindBelongsToAdapter extends EmberObject { - findRecord() { - findRecordCalled++; - } - - findBelongsTo(passedStore, snapshot, url, relationship) { - findBelongsToCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findBelongsTo'); - - let expectedURL = initialRecord.data.relationships.post.links.related; - assert.strictEqual(url, expectedURL, 'url is passed to findBelongsTo'); - assert.strictEqual(relationship.key, 'post', 'relationship is passed to findBelongsTo'); - - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findBelongsTo with correct modelName'); - assert.strictEqual(snapshot.id, '3', 'snapshot is passed to findBelongsTo with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindBelongsToAdapter); - - let comment = store.push(initialRecord); - - let post = await comment.post; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findBelongsToCalled, 1, 'findBelongsTo is called once'); - assert.deepEqual(post.serialize(), expectedResult, 'findBelongsTo returns expected result'); - }); - - testInDebug( - 'if a belongsTo relationship has a link but no data (findBelongsTo is undefined)', - async function (assert) { - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - links: { - related: 'https://example.com/api/post/2', - }, - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestFindBelongsToAdapter extends EmberObject {} - - owner.register('adapter:application', TestFindBelongsToAdapter); - - let comment = store.push(initialRecord); - - await assert.expectAssertion(async function () { - await comment.post; - }, /You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'/); - } - ); - - test('if a belongsTo relationship has data but not a link (findBelongsTo is defined)', async function (assert) { - let findRecordCalled = 0; - let findBelongsToCalled = 0; - - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - }, - }, - }, - }; - - let expectedResult = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - findRecord(passedStore, type, id, snapshot) { - findRecordCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Post, 'model is passed to findRecord'); - assert.strictEqual(id, '2', 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultCopy); - } - - findBelongsTo() { - findBelongsToCalled++; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let comment = store.push(initialRecord); - - let post = await comment.post; - - assert.strictEqual(findRecordCalled, 1, 'findRecord is called once'); - assert.strictEqual(findBelongsToCalled, 0, 'findBelongsTo is not called'); - assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); - }); - - test('if a belongsTo relationship has data but not a link (findBelongsTo is not defined)', async function (assert) { - let findRecordCalled = 0; - - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - }, - }, - }, - }; - - let expectedResult = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - findRecord(passedStore, type, id, snapshot) { - findRecordCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Post, 'model is passed to findRecord'); - assert.strictEqual(id, '2', 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let comment = store.push(initialRecord); - - let post = await comment.post; - - assert.strictEqual(findRecordCalled, 1, 'findRecord is called once'); - assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); - }); - - test('if a belongsTo relationship has a link and data (findBelongsTo is defined)', async function (assert) { - let findRecordCalled = 0; - let findBelongsToCalled = 0; - - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - links: { - related: 'https://example.com/api/post/2', - }, - }, - }, - }, - }; - - let expectedResult = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindBelongsToAdapter extends EmberObject { - findRecord() { - findRecordCalled++; - } - - findBelongsTo(passedStore, snapshot, url, relationship) { - findBelongsToCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findBelongsTo'); - - let expectedURL = initialRecord.data.relationships.post.links.related; - assert.strictEqual(url, expectedURL, 'url is passed to findBelongsTo'); - assert.strictEqual(relationship.name, 'post', 'relationship is passed to findBelongsTo'); - - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findBelongsTo with correct modelName'); - assert.strictEqual(snapshot.id, '3', 'snapshot is passed to findBelongsTo with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindBelongsToAdapter); - - let comment = store.push(initialRecord); - - let post = await comment.post; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findBelongsToCalled, 1, 'findBelongsTo is called once'); - assert.deepEqual(post.serialize(), expectedResult, 'findBelongsTo returns expected result'); - }); - - test('if a belongsTo relationship has link and data (findBelongsTo is not defined)', async function (assert) { - let findRecordCalled = 0; - - let initialRecord = { - data: { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - }, - }, - }, - }; - - let expectedResult = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - findRecord(passedStore, type, id, snapshot) { - findRecordCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Post, 'model is passed to findRecord'); - assert.strictEqual(id, '2', 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let comment = store.push(initialRecord); - - let post = await comment.post; - - assert.strictEqual(findRecordCalled, 1, 'findRecord is called once'); - assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/coalescing-test.js b/tests/adapter-encapsulation/tests/integration/coalescing-test.js deleted file mode 100644 index c39030f832c..00000000000 --- a/tests/adapter-encapsulation/tests/integration/coalescing-test.js +++ /dev/null @@ -1,754 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { all, resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; -import { recordIdentifierFor } from '@ember-data/store'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - return { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - }, - }; - } -} - -class Person extends Model { - @attr - firstName; - - @attr - lastName; -} - -module('integration/coalescing - Coalescing Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:person', Person); - }); - - test('coalesceFindRequests is true and findMany is not defined', async function (assert) { - let findRecordCalled = 0; - - let expectedResults = [ - { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }, - { - data: { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - }, - ]; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultsCopy = deepCopy(expectedResults); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord(passedStore, type, id, snapshot) { - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Person, 'model is passed to findRecord'); - - let expectedId = expectedResultsCopy[findRecordCalled].data.id; - assert.strictEqual(id, expectedId, 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultsCopy[findRecordCalled++]); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let promises = expectedResults.map((result) => result.data.id).map((id) => store.findRecord('person', id)); - let records = await all(promises); - - let serializedRecords = records.map((record) => record.serialize()); - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); - }); - - test('coalesceFindRequests is true and findMany is defined', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let groupRecordsForFindManyCalled = 0; - - let expectedResults = { - data: [ - { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - ], - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultsCopy = deepCopy(expectedResults); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Person, 'model is passed to findMany'); - - let expectedIds = expectedResultsCopy.data.map((record) => record.id); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResultsCopy); - } - - groupRecordsForFindMany(store, snapshots) { - groupRecordsForFindManyCalled++; - return [snapshots]; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); - let records = await all(promises); - - let serializedRecords = records.slice().map((record) => record.serialize()); - expectedResults = expectedResults.data.map((result) => ({ data: result })); - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.strictEqual(groupRecordsForFindManyCalled, 1, 'groupRecordsForFindMany is called once'); - assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); - }); - - test('Coalescing works with multiple includes options specified (bypass findMany)', async function (assert) { - let findRecordCalled = 0; - - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord(_store, _schema, id, snapshot) { - findRecordCalled++; - - return { - data: - id === '1' - ? { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - } - : { - id: '2', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - }; - } - - findMany() { - throw new Error(`We should not call findMany`); - } - - groupRecordsForFindMany() { - throw new Error(`We should not call groupRecordForFindMany`); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let person1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); - let person2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '2' }); - let promises = [ - store.findRecord('person', '1'), // creates request (1) - store.findRecord('person', '1', { include: '' }), // de-duped - store.findRecord('person', '1', { include: 'users' }), // creates request (2) - store.findRecord('person', '1', { include: 'users', adapterOptions: { opt: '1' } }), // creates request (3) - store.findRecord('person', '1', { include: 'users', adapterOptions: { opt: '2' } }), // creates request (4) - store.findRecord('person', '1', { include: 'users' }), // de-duped - store.findRecord('person', '1', { adapterOptions: { opt: '2' } }), // creates request (5) - store.findRecord('person', '1', { include: 'users.foo' }), // creates request (6) - store.findRecord('person', '2', { include: 'users.foo' }), // creates request (7) - store.findRecord('person', '2', { include: 'users' }), // creates request (8) - store.findRecord('person', '2', { include: 'users' }), // de-duped - store.findRecord('person', '2', { include: '' }), // de-duped - store.findRecord('person', '2'), // de-duped - store.findRecord('person', '2', { include: 'users' }), // de-duped - store.findRecord('person', '2', { include: 'users.foo' }), // de-duped - ]; - let records = await all(promises); - let foundIdentifiers = records.map((record) => recordIdentifierFor(record)); - let expectedIdentifiers = [ - person1, - person1, - person1, - person1, - person1, - person1, - person1, - person1, - person2, - person2, - person2, - person2, - person2, - person2, - person2, - ]; - - assert.strictEqual(findRecordCalled, 8, 'findRecord is called 8x'); - assert.deepEqual(foundIdentifiers, expectedIdentifiers, 'each findRecord returns expected result'); - - const person1record = store.peekRecord('person', '1'); - const person2record = store.peekRecord('person', '2'); - assert.strictEqual(person1record.firstName, 'Gaurav', 'person 1 loaded'); - assert.strictEqual(person2record.firstName, 'Chris', 'person 2 loaded'); - }); - - test('Coalescing works with multiple includes options specified (uses findMany)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let groupRecordsForFindManyCalled = 0; - - let expectedResults = { - data: [ - { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - { - id: '2', - type: 'person', - attributes: { - firstName: 'Wesley', - lastName: 'Thoburn', - }, - }, - { - id: '3', - type: 'person', - attributes: { - firstName: 'James', - lastName: 'Thoburn', - }, - }, - { - id: '4', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - ], - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - - return { data: null }; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Person, 'model is passed to findMany'); - - let expectedIds = ['1', '2', '3', '4']; - let expectedIncludes = [undefined, 'users', 'users.foo', ['comments']]; - let expectedOptions = [undefined, undefined, { opt: '1' }, { opt: '2' }]; - let includes = snapshots.map((snapshot) => snapshot.include); - let options = snapshots.map((snapshot) => snapshot.adapterOptions); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - assert.deepEqual(includes, expectedIncludes, 'includes are what was expected'); - assert.deepEqual(options, expectedOptions, 'options are what was expected'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResults); - } - - groupRecordsForFindMany(_store, snapshots) { - groupRecordsForFindManyCalled++; - return [snapshots]; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let person1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); - let person2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '2' }); - let person3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '3' }); - let person4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '4' }); - let promises = [ - store.findRecord('person', '1'), - store.findRecord('person', '2', { include: 'users' }), - store.findRecord('person', '3', { include: 'users.foo', adapterOptions: { opt: '1' } }), - store.findRecord('person', '4', { include: ['comments'], adapterOptions: { opt: '2' } }), - ]; - let records = await all(promises); - let foundIdentifiers = records.map((record) => recordIdentifierFor(record)); - let expectedIdentifiers = [person1, person2, person3, person4]; - expectedResults = expectedResults.data.map((result) => ({ data: result })); - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.strictEqual(groupRecordsForFindManyCalled, 1, 'groupRecordsForFindMany is called once'); - assert.deepEqual(foundIdentifiers, expectedIdentifiers, 'each findRecord returns expected result'); - - const person1record = store.peekRecord('person', '1'); - const person2record = store.peekRecord('person', '2'); - const person3record = store.peekRecord('person', '3'); - const person4record = store.peekRecord('person', '4'); - assert.strictEqual(person1record.firstName, 'Gaurav', 'person 1 loaded'); - assert.strictEqual(person2record.firstName, 'Wesley', 'person 2 loaded'); - assert.strictEqual(person3record.firstName, 'James', 'person 3 loaded'); - assert.strictEqual(person4record.firstName, 'Chris', 'person 4 loaded'); - }); - - test('coalesceFindRequests is true and findMany is defined but groupRecordsForFindMany is undefined', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - - let expectedResults = { - data: [ - { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - ], - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - let expectedResultsCopy = deepCopy(expectedResults); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Person, 'model is passed to findMany'); - - let expectedIds = expectedResultsCopy.data.map((record) => record.id); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResultsCopy); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); - let records = await all(promises); - - let serializedRecords = records.slice().map((record) => record.serialize()); - expectedResults = expectedResults.data.map((result) => ({ data: result })); - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); - }); - - test('coalesceFindRequests is false', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let groupRecordsForFindManyCalled = 0; - - let expectedResults = [ - { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }, - { - data: { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - }, - ]; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultsCopy = deepCopy(expectedResults); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = false; - - findRecord(passedStore, type, id, snapshot) { - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Person, 'model is passed to findRecord'); - - let expectedId = expectedResultsCopy[findRecordCalled].data.id; - assert.strictEqual(id, expectedId, 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultsCopy[findRecordCalled++]); - } - - findMany() { - findManyCalled++; - } - - groupRecordsForFindMany(store, snapshots) { - groupRecordsForFindManyCalled++; - return [snapshots]; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let promises = expectedResults.map((result) => result.data.id).map((id) => store.findRecord('person', id)); - let records = await all(promises); - - let serializedRecords = records.map((record) => record.serialize()); - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.strictEqual(groupRecordsForFindManyCalled, 0, 'groupRecordsForFindMany is not called'); - assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); - }); - - test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord', async function (assert) { - let findRecordCalled = 0; - - let { owner } = this; - let store = owner.lookup('service:store'); - const options = [ - undefined, // not de-duped since is first request seen - { reload: true }, // de-dupe - { backgroundReload: true }, // de-dupe - { reload: true, include: 'comments,friends' }, // should produce a request - { reload: true, include: ['friends', 'comments'] }, // de-dupe - { reload: true, include: 'friends,comments' }, // de-dupe - { reload: true, include: 'notFriends,comments' }, // should produce a request - { reload: true, include: 'comments' }, // de-dupe since included in comments,friends - { reload: true, include: 'friends' }, // de-dupe since included in comments,friends - ]; - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - async findRecord(_store, _schema, _id, snapshot) { - findRecordCalled++; - - if (findRecordCalled === 1) { - assert.strictEqual(snapshot.include, undefined, 'No include for first request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for first request'); - } else if (findRecordCalled === 2) { - assert.strictEqual(snapshot.include, 'comments,friends', 'include is correct for second request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); - } else if (findRecordCalled === 3) { - assert.strictEqual(snapshot.include, 'notFriends,comments', 'include is correct for third request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); - } - - return { - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - } - - async findMany() { - throw new Error(`Expected findMany to not be called`); - } - - groupRecordsForFindMany() { - throw new Error(`Expected groupRecordsForFindMany to not be called`); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - const request = store.findRecord('person', '1', options[0]); - const request2 = store.findRecord('person', '1', options[1]); - const request3 = store.findRecord('person', '1', options[2]); - const request4 = store.findRecord('person', '1', options[3]); - const request5 = store.findRecord('person', '1', options[4]); - const request6 = store.findRecord('person', '1', options[5]); - const request7 = store.findRecord('person', '1', options[6]); - const request8 = store.findRecord('person', '1', options[7]); - const request9 = store.findRecord('person', '1', options[8]); - - await all([request, request2, request3, request4, request5, request6, request7, request8, request9]); - - assert.strictEqual(store.peekAll('person').length, 1, 'only one record is in the store'); - - assert.strictEqual(findRecordCalled, 3, 'findRecord is called three times'); - }); - - test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord (order scenario 2)', async function (assert) { - let findRecordCalled = 0; - - let { owner } = this; - let store = owner.lookup('service:store'); - const options = [ - { include: 'comments' }, // not de-duped since first request - { reload: true, include: 'comments,friends' }, // should produce a request - undefined, // de-dupe - { reload: true }, // de-dupe - { backgroundReload: true }, // de-dupe - { reload: true, include: ['friends', 'comments'] }, // de-dupe - { reload: true, include: 'friends,comments' }, // de-dupe - { reload: true, include: 'notFriends,comments' }, // should produce a request - { reload: true, include: 'friends' }, // de-dupe since included in comments,friends - ]; - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - async findRecord(_store, _schema, _id, snapshot) { - findRecordCalled++; - - if (findRecordCalled === 1) { - assert.strictEqual(snapshot.include, 'comments', 'include for first request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for first request'); - } else if (findRecordCalled === 2) { - assert.strictEqual(snapshot.include, 'comments,friends', 'include is correct for second request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); - } else if (findRecordCalled === 3) { - assert.strictEqual(snapshot.include, 'notFriends,comments', 'include is correct for third request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); - } - - return { - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - } - - async findMany() { - throw new Error(`Expected findMany to not be called`); - } - - groupRecordsForFindMany() { - throw new Error(`Expected groupRecordsForFindMany to not be called`); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - const request = store.findRecord('person', '1', options[0]); - const request2 = store.findRecord('person', '1', options[1]); - const request3 = store.findRecord('person', '1', options[2]); - const request4 = store.findRecord('person', '1', options[3]); - const request5 = store.findRecord('person', '1', options[4]); - const request6 = store.findRecord('person', '1', options[5]); - const request7 = store.findRecord('person', '1', options[6]); - const request8 = store.findRecord('person', '1', options[7]); - const request9 = store.findRecord('person', '1', options[8]); - - await all([request, request2, request3, request4, request5, request6, request7, request8, request9]); - - assert.strictEqual(store.peekAll('person').length, 1, 'only one record is in the store'); - - assert.strictEqual(findRecordCalled, 3, 'findRecord is called three times'); - }); - - test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord (order scenario 3)', async function (assert) { - let findRecordCalled = 0; - - let { owner } = this; - let store = owner.lookup('service:store'); - const options = [ - { reload: true, include: 'comments,friends' }, // not de-duped since first request - undefined, // de-dupe - { reload: true }, // de-dupe - { backgroundReload: true }, // de-dupe - { reload: true, include: ['friends', 'comments'] }, // de-dupe - { reload: true, include: 'friends,comments' }, // de-dupe - { reload: true, include: 'notFriends,comments' }, // should produce a request - { include: 'comments' }, // de-dupe - { reload: true, include: 'friends' }, // de-dupe since included in comments,friends - ]; - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = true; - - async findRecord(_store, _schema, _id, snapshot) { - findRecordCalled++; - - if (findRecordCalled === 1) { - assert.strictEqual(snapshot.include, 'comments,friends', 'include is correct for second request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); - } else if (findRecordCalled === 2) { - assert.strictEqual(snapshot.include, 'notFriends,comments', 'include is correct for third request'); - assert.strictEqual(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); - } - - return { - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - } - - async findMany() { - throw new Error(`Expected findMany to not be called`); - } - - groupRecordsForFindMany() { - throw new Error(`Expected groupRecordsForFindMany to not be called`); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - const request = store.findRecord('person', '1', options[0]); - const request2 = store.findRecord('person', '1', options[1]); - const request3 = store.findRecord('person', '1', options[2]); - const request4 = store.findRecord('person', '1', options[3]); - const request5 = store.findRecord('person', '1', options[4]); - const request6 = store.findRecord('person', '1', options[5]); - const request7 = store.findRecord('person', '1', options[6]); - const request8 = store.findRecord('person', '1', options[7]); - const request9 = store.findRecord('person', '1', options[8]); - - await all([request, request2, request3, request4, request5, request6, request7, request8, request9]); - - assert.strictEqual(store.peekAll('person').length, 1, 'only one record is in the store'); - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/generate-id-test.js b/tests/adapter-encapsulation/tests/integration/generate-id-test.js deleted file mode 100644 index 396cccb5e9d..00000000000 --- a/tests/adapter-encapsulation/tests/integration/generate-id-test.js +++ /dev/null @@ -1,101 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - return { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - }, - }; - } -} - -class Person extends Model { - @attr - firstName; - - @attr - lastName; -} - -module('integration/generate-id - GenerateIdForRecord Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:person', Person); - }); - - test('store.createRecord calls adapter.generateIdForRecord if defined and we use this ID for the record', async function (assert) { - let generateIdForRecordCalled = 0; - let seq = 0; - - let store = this.owner.lookup('service:store'); - let expectedProps = { - firstName: 'Gaurav', - lastName: 'Munjal', - }; - - class TestGenerateIdForRecordAdapter extends EmberObject { - generateIdForRecord() { - generateIdForRecordCalled++; - - return 'manually generated id ' + ++seq; - } - } - - this.owner.register('adapter:application', TestGenerateIdForRecordAdapter); - - let record = store.createRecord('person', expectedProps); - - assert.strictEqual(record.id, 'manually generated id 1', 'manually generated id used'); - - let recordFromPeekRecord = store.peekRecord('person', record.id); - - assert.strictEqual(record, recordFromPeekRecord, 'peekRecord returns the same record'); - assert.strictEqual(generateIdForRecordCalled, 1, 'generateIdForRecord is called once'); - assert.deepEqual(record.serialize(), { - data: { - id: 'manually generated id 1', - type: 'person', - attributes: expectedProps, - }, - }); - }); - - test('store.createRecord does not error if adapter.generateIdForRecord is undefined.', async function (assert) { - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestGenerateIdForRecordAdapter extends EmberObject {} - - this.owner.register('adapter:application', TestGenerateIdForRecordAdapter); - - let props = expectedData.data.attributes; - let record = store.createRecord('person', props); - - assert.deepEqual(record.serialize().data.attributes, props, 'record created without error'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/has-many-test.js b/tests/adapter-encapsulation/tests/integration/has-many-test.js deleted file mode 100644 index 0829e788a2c..00000000000 --- a/tests/adapter-encapsulation/tests/integration/has-many-test.js +++ /dev/null @@ -1,780 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; -import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - let json = { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - relationships: {}, - }, - }; - - snapshot.eachRelationship((key, relationship) => { - if (relationship.kind === 'belongsTo') { - this.serializeBelongsTo(snapshot, json, relationship); - } else if (relationship.kind === 'hasMany') { - this.serializeHasMany(snapshot, json, relationship); - } - }); - - if (Object.keys(json.data.relationships).length === 0) { - delete json.data.relationships; - } - - return json; - } - - // minimal implementation, not json-api compliant - serializeBelongsTo(snapshot, json, relationship) { - let key = relationship.key; - let belongsTo = snapshot.belongsTo(key); - - if (belongsTo) { - let value = { - data: { - id: belongsTo.id, - type: belongsTo.modelName, - }, - }; - json.data.relationships[key] = value; - } - } - - // minimal implementation, not json-api compliant - serializeHasMany(snapshot, json, relationship) { - let key = relationship.key; - let hasMany = snapshot.hasMany(key); - - if (hasMany && hasMany.length) { - let value = { - data: hasMany.map((snap) => ({ - id: snap.id, - type: snap.modelName, - })), - }; - json.data.relationships[key] = value; - } - } -} - -class Post extends Model { - @attr - text; - - @hasMany('comments', { async: true, inverse: 'post' }) - comments; -} - -class Comment extends Model { - @attr - text; - - @belongsTo('post', { async: true, inverse: 'comments' }) - post; -} - -let expectedResult = { - data: [ - { - id: '3', - type: 'comment', - attributes: { - text: 'You rock', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - }, - }, - }, - { - id: '4', - type: 'comment', - attributes: { - text: 'You rock too', - }, - relationships: { - post: { - data: { - id: '2', - type: 'post', - }, - }, - }, - }, - ], -}; - -module('integration/has-many - Has Many Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:post', Post); - this.owner.register('model:comment', Comment); - }); - - test('if a hasMany relationship has a link but no data (findHasMany is defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let findHasManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - links: { - related: 'https://example.com/api/post/2/comments', - }, - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindHasManyAdapter extends EmberObject { - findRecord() { - findRecordCalled++; - } - - findMany() { - findManyCalled++; - } - - findHasMany(passedStore, snapshot, url, relationship) { - findHasManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findHasMany'); - - let expectedURL = initialRecord.data.relationships.comments.links.related; - assert.strictEqual(url, expectedURL, 'url is passed to findHasMany'); - assert.strictEqual(relationship.name, 'comments', 'relationship is passed to findHasMany'); - - assert.strictEqual(snapshot.modelName, 'post', 'snapshot is passed to findHasMany with correct modelName'); - assert.strictEqual(snapshot.id, '2', 'snapshot is passed to findHasMany with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindHasManyAdapter); - - let post = store.push(initialRecord); - - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.strictEqual(findHasManyCalled, 1, 'findHasMany is called once'); - assert.deepEqual(serializedComments, expectedResult, 'findHasMany returns expected result'); - }); - - testInDebug('if a hasMany relationship has a link but no data (findHasMany is undefined)', async function (assert) { - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - links: { - related: 'https://example.com/api/post/2/comments', - }, - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestFindHasManyAdapter extends EmberObject {} - - owner.register('adapter:application', TestFindHasManyAdapter); - - let post = store.push(initialRecord); - - await assert.expectAssertion(async function () { - await post.comments; - }, /You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'/); - }); - - test('if a hasMany relationship has data but not a link (coalescing is off, findHasMany is defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let findHasManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = false; - - findRecord(passedStore, type, id, snapshot) { - let index = findRecordCalled++; - let expectedId = initialRecord.data.relationships.comments.data[index].id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Comment, 'model is passed to findRecord'); - assert.strictEqual(id, expectedId, 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); - - return resolve({ data: expectedResultCopy.data[index] }); - } - - findMany() { - findManyCalled++; - } - - findHasMany() { - findHasManyCalled++; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.strictEqual(findHasManyCalled, 0, 'findHasMany is not called'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); - - test('if a hasMany relationship has data but not a link (coalescing is off, findHasMany is not defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = false; - - findRecord(passedStore, type, id, snapshot) { - let index = findRecordCalled++; - let expectedId = initialRecord.data.relationships.comments.data[index].id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Comment, 'model is passed to findRecord'); - assert.strictEqual(id, expectedId, 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); - - return resolve({ data: expectedResultCopy.data[index] }); - } - - findMany() { - findManyCalled++; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); - - test('if a hasMany relationship has data but not a link (coalescing is on, findHasMany is defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let findHasManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindManyAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - } - - findHasMany() { - findHasManyCalled++; - } - - groupRecordsForFindMany(store, snapshots) { - return [snapshots]; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Comment, 'model is passed to findMany'); - - let expectedIds = expectedResultCopy.data.map((record) => record.id); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindManyAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.strictEqual(findHasManyCalled, 0, 'findHasMany is not called'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); - - test('if a hasMany relationship has data but not a link (coalescing is on, findHasMany is not defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindManyAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - } - - groupRecordsForFindMany(store, snapshots) { - return [snapshots]; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Comment, 'model is passed to findMany'); - - let expectedIds = expectedResultCopy.data.map((record) => record.id); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindManyAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); - - test('if a hasMany relationship has link and data (findHasMany is defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - let findHasManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - links: { - related: 'https://example.com/api/post/2/comments', - }, - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindHasManyAdapter extends EmberObject { - findRecord() { - findRecordCalled++; - } - - findMany() { - findManyCalled++; - } - - findHasMany(passedStore, snapshot, url, relationship) { - findHasManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findHasMany'); - - let expectedURL = initialRecord.data.relationships.comments.links.related; - assert.strictEqual(url, expectedURL, 'url is passed to findHasMany'); - assert.strictEqual(relationship.name, 'comments', 'relationship is passed to findHasMany'); - - assert.strictEqual(snapshot.modelName, 'post', 'snapshot is passed to findHasMany with correct modelName'); - assert.strictEqual(snapshot.id, '2', 'snapshot is passed to findHasMany with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindHasManyAdapter); - - let post = store.push(initialRecord); - - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.strictEqual(findHasManyCalled, 1, 'findHasMany is called once'); - assert.deepEqual(serializedComments, expectedResult, 'findHasMany returns expected result'); - }); - - test('if a hasMany relationship has link and data (coalescing is on, findHasMany is not defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - links: { - related: 'https://example.com/api/post/2/comments', - }, - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindManyAdapter extends EmberObject { - coalesceFindRequests = true; - - findRecord() { - findRecordCalled++; - } - - groupRecordsForFindMany(store, snapshots) { - return [snapshots]; - } - - findMany(passedStore, type, ids, snapshots) { - findManyCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findMany'); - assert.strictEqual(type, Comment, 'model is passed to findMany'); - - let expectedIds = expectedResultCopy.data.map((record) => record.id); - assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); - - snapshots.forEach((snapshot, index) => { - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); - assert.strictEqual(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); - }); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindManyAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 0, 'findRecord is not called'); - assert.strictEqual(findManyCalled, 1, 'findMany is called once'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); - - test('if a hasMany relationship has link and data (coalescing is off, findHasMany is not defined)', async function (assert) { - let findRecordCalled = 0; - let findManyCalled = 0; - - let initialRecord = { - data: { - id: '2', - type: 'post', - attributes: { - text: "I'm awesome", - }, - relationships: { - comments: { - data: [ - { - id: '3', - type: 'comment', - }, - { - id: '4', - type: 'comment', - }, - ], - }, - }, - }, - }; - - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - coalesceFindRequests = false; - - findRecord(passedStore, type, id, snapshot) { - let index = findRecordCalled++; - let expectedId = initialRecord.data.relationships.comments.data[index].id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Comment, 'model is passed to findRecord'); - assert.strictEqual(id, expectedId, 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); - - return resolve({ data: expectedResultCopy.data[index] }); - } - - findMany() { - findManyCalled++; - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let post = store.push(initialRecord); - let comments = await post.comments; - let serializedComments = { - data: comments.slice().map((comment) => comment.serialize().data), - }; - - assert.strictEqual(findRecordCalled, 2, 'findRecord is called twice'); - assert.strictEqual(findManyCalled, 0, 'findMany is not called'); - assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/mutations-test.js b/tests/adapter-encapsulation/tests/integration/mutations-test.js deleted file mode 100644 index c403f8f6cfc..00000000000 --- a/tests/adapter-encapsulation/tests/integration/mutations-test.js +++ /dev/null @@ -1,336 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - return { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - }, - }; - } -} - -class Person extends Model { - @attr - firstName; - - @attr - lastName; -} - -module('integration/mutations - Mutations Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:person', Person); - }); - - test('store.deleteRecord calls adapter.deleteRecord if a record is deleted and then saved', async function (assert) { - let deleteRecordCalled = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestDeleteRecordAdapter extends EmberObject { - deleteRecord(passedStore, type, snapshot) { - deleteRecordCalled++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - } - } - - this.owner.register('adapter:application', TestDeleteRecordAdapter); - - let record = store.push(expectedData); - - record.deleteRecord(); - await record.save(); - - assert.strictEqual(deleteRecordCalled, 1, 'deleteRecord is called once'); - }); - - test('store.deleteRecord calls adapter.deleteRecord if a newly created record is persisted, then deleted and then saved', async function (assert) { - let createRecordCalled = 0; - let deleteRecordCalled = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestDeleteRecordAdapter extends EmberObject { - createRecord(passedStore, type, snapshot) { - createRecordCalled++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - - return resolve(data); - } - - deleteRecord(passedStore, type, snapshot) { - deleteRecordCalled++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - } - } - - this.owner.register('adapter:application', TestDeleteRecordAdapter); - - let props = { id: expectedData.data.id, ...expectedData.data.attributes }; - let record = store.createRecord('person', props); - await record.save(); - - assert.strictEqual(createRecordCalled, 1, 'createRecord is called once'); - - record.deleteRecord(); - await record.save(); - - assert.strictEqual(deleteRecordCalled, 1, 'deleteRecord is called once'); - }); - - test('store.deleteRecord does not call adapter.deleteRecord if a newly created, unpersisted record is deleted and then saved', async function (assert) { - let createRecordCalled = 0; - let deleteRecordCalled = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestDeleteRecordAdapter extends EmberObject { - createRecord(passedStore, type, snapshot) { - createRecordCalled++; - } - - deleteRecord(passedStore, type, snapshot) { - deleteRecordCalled++; - } - } - - this.owner.register('adapter:application', TestDeleteRecordAdapter); - - let props = { id: expectedData.data.id, ...expectedData.data.attributes }; - let record = store.createRecord('person', props); - - record.deleteRecord(); - await record.save(); - - assert.strictEqual(createRecordCalled, 0, 'adapter.createRecord is not called'); - assert.strictEqual(deleteRecordCalled, 0, 'adapter.deleteRecord is not called'); - }); - - test('record.save() calls adapter.createRecord if a newly created record unpersisted record is saved', async function (assert) { - let createRecordCalled = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestCreateRecordAdapter extends EmberObject { - createRecord(passedStore, type, snapshot) { - createRecordCalled++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - - return resolve(data); - } - } - - this.owner.register('adapter:application', TestCreateRecordAdapter); - - let props = { id: expectedData.data.id, ...expectedData.data.attributes }; - let record = store.createRecord('person', props); - await record.save(); - - assert.strictEqual(createRecordCalled, 1, 'createRecord is called once'); - }); - - test('record.save() calls adapter.createRecord then adapter.updateRecord if a newly created record record is saved, then saved again', async function (assert) { - let createRecordCalled = 0; - let updateRecord = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestUpdateRecordAdapter extends EmberObject { - createRecord(passedStore, type, snapshot) { - createRecordCalled++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - - return resolve(data); - } - - updateRecord(passedStore, type, snapshot) { - updateRecord++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - - return resolve(expectedData); - } - } - - this.owner.register('adapter:application', TestUpdateRecordAdapter); - - let props = { id: expectedData.data.id, ...expectedData.data.attributes }; - let record = store.createRecord('person', props); - await record.save(); - - assert.strictEqual(createRecordCalled, 1, 'createRecord is called once'); - - record.firstName = 'Kevin'; - expectedData.data.attributes.firstName = 'Kevin'; - await record.save(); - - assert.strictEqual(createRecordCalled, 1, 'createRecord is not called again'); - assert.strictEqual(updateRecord, 1, 'updateRecord is called once'); - }); - - test('record.save() calls adapter.updateRecord if an existing persisted record is saved', async function (assert) { - let createRecordCalled = 0; - let updateRecord = 0; - let store = this.owner.lookup('service:store'); - let expectedData = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - class TestUpdateRecordAdapter extends EmberObject { - createRecord(passedStore, type, snapshot) { - createRecordCalled++; - } - - updateRecord(passedStore, type, snapshot) { - updateRecord++; - - let data = snapshot.serialize(); - let id = snapshot.id; - - assert.strictEqual(passedStore, store, 'instance of store is passed to deleteRecord'); - assert.strictEqual(type, Person, 'model is passed to deleteRecord'); - assert.strictEqual(id, '12', 'id is passed to deleteRecord through snapshot'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); - assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); - - return resolve(expectedData); - } - } - - this.owner.register('adapter:application', TestUpdateRecordAdapter); - - let record = store.push(expectedData); - - record.firstName = 'Kevin'; - expectedData.data.attributes.firstName = 'Kevin'; - await record.save(); - - assert.strictEqual(createRecordCalled, 0, 'createRecord is not called'); - assert.strictEqual(updateRecord, 1, 'updateRecord is called once'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/queries-test.js b/tests/adapter-encapsulation/tests/integration/queries-test.js deleted file mode 100644 index c644950cda6..00000000000 --- a/tests/adapter-encapsulation/tests/integration/queries-test.js +++ /dev/null @@ -1,281 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } - - serialize(snapshot) { - return { - data: { - id: snapshot.id, - type: snapshot.modelName, - attributes: snapshot.attributes(), - }, - }; - } -} - -class Person extends Model { - @attr - firstName; - - @attr - lastName; -} - -module('integration/queries - Queries Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:person', Person); - }); - - test('options passed to adapters by LegacyHandler are mutable', async function (assert) { - let { owner } = this; - let store = owner.lookup('service:store'); - - let expectedResult = { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }; - - class TestAdapter extends EmberObject { - query(passedStore, type, query, recordArray, adapterOptions) { - assert.deepEqual(query, { initialOption: 'foo' }, 'original query is passed to adapter'); - - query.initialOption = 'bar'; - adapterOptions ||= {}; - adapterOptions.otherOption = 'baz'; - - assert.strictEqual(query.initialOption, 'bar', 'query is mutated'); - assert.strictEqual(adapterOptions.otherOption, 'baz', 'adapterOptions is mutated'); - - return resolve({ - data: [expectedResult], - }); - } - queryRecord(passedStore, type, query, record, adapterOptions) { - assert.deepEqual(query, { initialOption: 'foo' }, 'original query is passed to adapter'); - - query.initialOption = 'bar'; - adapterOptions ||= {}; - adapterOptions.otherOption = 'baz'; - - assert.strictEqual(query.initialOption, 'bar', 'query is mutated'); - assert.strictEqual(adapterOptions.otherOption, 'baz', 'adapterOptions is mutated'); - - return resolve({ - data: expectedResult, - }); - } - } - - owner.register('adapter:application', TestAdapter); - - for (let method of ['query', 'queryRecord']) { - let result = await store[method]('person', { initialOption: 'foo' }); - assert.ok(result, `result is returned for ${method}`); - } - }); - - test('store.findRecord calls adapter.findRecord w/correct args', async function (assert) { - let findRecordCalled = 0; - let expectedResult = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - let { owner } = this; - let store = owner.lookup('service:store'); - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - class TestFindRecordAdapter extends EmberObject { - findRecord(passedStore, type, id, snapshot) { - findRecordCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findRecord'); - assert.strictEqual(type, Person, 'model is passed to findRecord'); - assert.strictEqual(id, '12', 'id is passed to findRecord'); - - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); - assert.strictEqual(snapshot.id, '12', 'snapshot is passed to findRecord with correct id'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindRecordAdapter); - - let record = await store.findRecord('person', '12'); - - assert.strictEqual(findRecordCalled, 1, 'findRecord is called once'); - assert.deepEqual(record.serialize(), expectedResult, 'findRecord returns expected result'); - }); - - test('store.findAll calls adapter.findAll w/correct args', async function (assert) { - let findAllCalled = 0; - let expectedResult = { - data: [ - { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - ], - }; - - // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 - // expectedResult is mutated during store.findRecord - // to add the lid - let expectedResultCopy = deepCopy(expectedResult); - - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestFindAllAdapter extends EmberObject { - findAll(passedStore, type, sinceToken, snapshot) { - findAllCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to findAll'); - assert.strictEqual(type, Person, 'model is passed to findAll'); - assert.strictEqual(sinceToken, null, 'sinceToken passed to findAll is null'); - assert.strictEqual(snapshot.modelName, 'person', 'snapshot is passed to findAll with correct modelName'); - assert.strictEqual(snapshot.length, 0, 'snapshot is passed to findAll represnts empty array'); - - return resolve(expectedResultCopy); - } - } - - owner.register('adapter:application', TestFindAllAdapter); - - let manyArray = await store.findAll('person'); - - let result = manyArray.slice().map((person) => person.serialize()); - expectedResult = expectedResult.data.map((person) => ({ data: person })); - - assert.strictEqual(findAllCalled, 1, 'findAll is called once'); - assert.deepEqual(result, expectedResult, 'findAll returns expected result'); - }); - - test('store.queryRecord calls adapter.queryRecord w/correct args', async function (assert) { - let queryRecordCalled = 0; - let expectedResult = { - data: { - id: '12', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestQueryRecordAdapter extends EmberObject { - queryRecord(passedStore, type, query, options) { - queryRecordCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to queryRecord'); - assert.strictEqual(type, Person, 'model is passed to queryRecord'); - assert.deepEqual(query, { firstName: 'Gaurav' }, 'query is passed to queryRecord'); - assert.deepEqual(options, {}, 'options is passsed to queryRecord'); - - return resolve(expectedResult); - } - } - - owner.register('adapter:application', TestQueryRecordAdapter); - - let record = await store.queryRecord('person', { firstName: 'Gaurav' }); - - assert.strictEqual(queryRecordCalled, 1, 'queryRecord is called once'); - assert.deepEqual(record.serialize(), expectedResult, 'queryRecord returns expected result'); - }); - - test('store.query calls adapter.query w/correct args', async function (assert) { - let queryCalled = 0; - let expectedResult = { - data: [ - { - id: '14', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Tse', - }, - }, - { - id: '19', - type: 'person', - attributes: { - firstName: 'Chris', - lastName: 'Thoburn', - }, - }, - ], - }; - let { owner } = this; - let store = owner.lookup('service:store'); - - class TestQueryAdapter extends EmberObject { - query(passedStore, type, query, recordArray, options) { - queryCalled++; - - assert.strictEqual(passedStore, store, 'instance of store is passed to query'); - assert.strictEqual(type, Person, 'model is passed to query'); - assert.deepEqual(query, { firstName: 'Chris' }, 'query is passed to query'); - assert.deepEqual(recordArray.slice(), [], 'recordArray is passsed to query'); - assert.deepEqual(options, {}, 'options is passed to query'); - - return resolve(expectedResult); - } - } - - owner.register('adapter:application', TestQueryAdapter); - - let manyArray = await store.query('person', { firstName: 'Chris' }); - - let result = manyArray.slice().map((person) => person.serialize()); - expectedResult = expectedResult.data.map((person) => ({ data: person })); - - assert.strictEqual(queryCalled, 1, 'query is called once'); - assert.deepEqual(result, expectedResult, 'query returns expected result'); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/reload-test.js b/tests/adapter-encapsulation/tests/integration/reload-test.js deleted file mode 100644 index 3804c7d2b7d..00000000000 --- a/tests/adapter-encapsulation/tests/integration/reload-test.js +++ /dev/null @@ -1,674 +0,0 @@ -import EmberObject from '@ember/object'; - -import Store from 'adapter-encapsulation-test-app/services/store'; -import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; - -import { setupTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; - -class MinimalSerializer extends EmberObject { - normalizeResponse(_, __, data) { - return data; - } -} - -class Person extends Model { - @attr - firstName; - - @attr - lastName; -} - -function setupReloadTest(options) { - class TestMinimumAdapter extends EmberObject { - shouldReloadAllCalled = 0; - shouldReloadRecordCalled = 0; - shouldBackgroundReloadAllCalled = 0; - shouldBackgroundReloadRecordCalled = 0; - - requestsMade = 0; - - constructor() { - super(...arguments); - - if (options.shouldReloadAll !== undefined) { - this.shouldReloadAll = function () { - this.shouldReloadAllCalled++; - return options.shouldReloadAll; - }; - } - - if (options.shouldReloadRecord !== undefined) { - this.shouldReloadRecord = function () { - this.shouldReloadRecordCalled++; - return options.shouldReloadRecord; - }; - } - if (options.shouldBackgroundReloadAll !== undefined) { - this.shouldBackgroundReloadAll = function () { - this.shouldBackgroundReloadAllCalled++; - return options.shouldBackgroundReloadAll; - }; - } - - if (options.shouldBackgroundReloadRecord !== undefined) { - this.shouldBackgroundReloadRecord = function () { - this.shouldBackgroundReloadRecordCalled++; - return options.shouldBackgroundReloadRecord; - }; - } - } - - findAll() { - this.requestsMade++; - return resolve(options.resolveFindAllWith || { data: [] }); - } - - findRecord() { - this.requestsMade++; - return resolve(options.resolveFindRecordWith || { data: null }); - } - } - this.owner.register('adapter:application', TestMinimumAdapter); - - this.store = this.owner.lookup('service:store'); - this.adapter = this.owner.lookup('adapter:application'); -} - -module('integration/reload - Reloading Tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - this.owner.register('serializer:application', MinimalSerializer); - this.owner.register('model:person', Person); - }); - - module('adapter.shouldReloadAll', function () { - test('adapter.shouldReloadAll is not called when store.findAll is called with a reload: false flag', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person', { reload: false }); - - assert.strictEqual(this.adapter.shouldReloadAllCalled, 0, 'shouldReloadAll is not called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no request is made'); - }); - - test('adapter.shouldReloadAll is not called when store.findAll is called with a reload: true flag', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person', { reload: true }); - - assert.strictEqual(this.adapter.shouldReloadAllCalled, 0, 'shouldReloadAll is not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('store.findAll does not error if adapter.shouldReloadAll is not defined (records are present)', async function (assert) { - setupReloadTest.call(this, { - shouldBackgroundReloadAll: false, - }); - - this.store.push({ - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('store.findAll does not error if adapter.shouldReloadAll is not defined (records are absent)', async function (assert) { - setupReloadTest.call(this, { - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldReloadAll is called when store.findAll is called without a reload flag (shouldReloadAll is false)', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.shouldReloadAllCalled, 1, 'shouldReloadAll is called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('adapter.shouldReloadAll is called when store.findAll is called without a reload flag (shouldReloadAll is false)', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: true, - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.shouldReloadAllCalled, 1, 'shouldReloadAll is called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - }); - - module('adapter.shouldBackgroundReloadAll', function () { - test('adapter.shouldBackgroundReloadAll is not called called when store.findAll is called with reload: true flag (but we do make request)', async function (assert) { - setupReloadTest.call(this, {}); - - await this.store.findAll('person', { reload: true }); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadAll is not called called when store.findAll is called and adaptershouldReloadAll() returns true (but we do make request)', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: true, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadAll is not called when store.findAll is called with backroundReload: true', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - }); - - await this.store.findAll('person', { backgroundReload: true }); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll is not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadAll is not called when store.findAll is called with backroundReload: false', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - }); - - await this.store.findAll('person', { backgroundReload: false }); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll is not called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('store.findAll does not error if adapter.shouldBackgroundReloadAll is undefined and backgroundReload is not present.', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadAll is called when store.findAll is called and there is no backgroundReload flag (returns true)', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - shouldBackgroundReloadAll: true, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 1, 'shouldBackgroundReloadAll is called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadAll is called when store.findAll is called and there is no backgroundReload flag (returns false)', async function (assert) { - setupReloadTest.call(this, { - shouldReloadAll: false, - shouldBackgroundReloadAll: false, - }); - - await this.store.findAll('person'); - - assert.strictEqual(this.adapter.shouldBackgroundReloadAllCalled, 1, 'shouldBackgroundReloadAll is called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - }); - - module('adapter.shouldReloadRecord', function () { - test('adapter.shouldReloadRecord is not called when store.findRecord is called for an unloaded record (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - let record = this.store.push(payload); - - this.store.unloadRecord(record); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldReloadRecord is not called when store.findRecord is called for a never loaded record (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldReloadRecord is not called when store.findRecord is called with a reload flag (but we do make request if reload is true)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1', { reload: true }); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldReloadRecord is not called when store.findRecord is called with a reload flag (and we do not make request if reload is false)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1', { reload: false }); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('if adapter.shouldReloadRecord is undefined, we default to false and do not make a request', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('adapter.shouldReloadRecord is called when store.findRecord is called without a reload flag (shouldReloadRecord returns true)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldReloadRecord: true, - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 1, 'shouldReloadRecord is called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldReloadRecord is called when store.findRecord is called without a reload flag (shouldReloadRecord returns false)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldReloadRecord: false, - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldReloadRecordCalled, 1, 'shouldReloadRecord is called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - }); - - module('adapter.shouldBackgroundReloadRecord', function () { - test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called for an unloaded record (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - let record = this.store.push(payload); - - this.store.unloadRecord(record); - - await this.store.findRecord('person', '1'); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called for a never loaded record (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - await this.store.findRecord('person', '1'); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is not called called when store.findRecord is called with reload: true flag (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1', { reload: true }); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is not called called when store.findRecord is called and shouldReloadRecord returns true (but we do make request)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldReloadRecord: true, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called with backroundReload as an option (backgroundReload is true)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1', { backgroundReload: true }); - await this.store._getAllPending(); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called with backroundReload as an option (backgroundReload is false)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1', { backgroundReload: false }); - - assert.strictEqual( - this.adapter.shouldBackgroundReloadRecordCalled, - 0, - 'shouldBackgroundReloadRecord is not called' - ); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - - test('store.findRecord does not error if adapter.shouldBackgroundReloadRecord is undefined and backgroundReload is not present.', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - await this.store._getAllPending(); - - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is called when store.findRecord is called and there is no backgroundReload flag (adapter.shouldBackgroundReloadRecord() returns true)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: true, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - await this.store._getAllPending(); - - assert.strictEqual(this.adapter.shouldBackgroundReloadRecordCalled, 1, 'shouldBackgroundReloadRecord is called'); - assert.strictEqual(this.adapter.requestsMade, 1, 'an ajax request is made'); - }); - - test('adapter.shouldBackgroundReloadRecord is called when store.findRecord is called and there is no backgroundReload flag (adapter.shouldBackgroundReloadRecord() returns false)', async function (assert) { - let payload = { - data: { - id: '1', - type: 'person', - attributes: { - firstName: 'Gaurav', - lastName: 'Munjal', - }, - }, - }; - - setupReloadTest.call(this, { - shouldBackgroundReloadRecord: false, - resolveFindRecordWith: payload, - }); - - this.store.push(payload); - - await this.store.findRecord('person', '1'); - - assert.strictEqual(this.adapter.shouldBackgroundReloadRecordCalled, 1, 'shouldBackgroundReloadRecord is called'); - assert.strictEqual(this.adapter.requestsMade, 0, 'no ajax request is made'); - }); - }); -}); diff --git a/tests/adapter-encapsulation/tests/integration/smoke-test.js b/tests/adapter-encapsulation/tests/integration/smoke-test.js deleted file mode 100644 index b5fcb0e9d3c..00000000000 --- a/tests/adapter-encapsulation/tests/integration/smoke-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* global require */ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -function assertPackageNotPresent(packageName, assert) { - const entries = Object.keys(require.entries); - const entriesFromPackage = entries.filter((m) => m.indexOf(packageName) === 0); - const importedDependencies = {}; - const entriesImportingPackage = entries.filter((m) => { - const deps = require.entries[m].deps; - const moduleDeps = deps.filter((d) => d.indexOf(packageName) === 0); - - if (moduleDeps.length) { - importedDependencies[m] = moduleDeps; - } - return moduleDeps.length > 0; - }); - - assert.ok(entries.length > 0, 'We have modules'); - assert.ok( - entriesFromPackage.length === 0, - `We expect no modules from ${packageName} ${ - entriesFromPackage.length > 0 ? `found: [\n\t"${entriesFromPackage.join('",\n\t"')}"\n]` : '' - }` - ); - assert.ok( - entriesImportingPackage.length === 0, - `We expect no modules with dependencies on ${packageName} ${ - entriesImportingPackage.length > 0 ? `found:\n${JSON.stringify(importedDependencies, null, 2)}` : '' - }` - ); -} - -module('Adapter Encapsulation - Smoke Tests', function (hooks) { - setupTest(hooks); - - test('No @ember-data/adapter modules are present', function (assert) { - assertPackageNotPresent('@ember-data/adapter', assert); - }); - - test('No ember-data modules are present', function (assert) { - assertPackageNotPresent('ember-data', assert); - }); -}); diff --git a/tests/adapter-encapsulation/tests/test-helper.js b/tests/adapter-encapsulation/tests/test-helper.js deleted file mode 100644 index a09192c58ec..00000000000 --- a/tests/adapter-encapsulation/tests/test-helper.js +++ /dev/null @@ -1,22 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/tests/blueprints/eslint.config.mjs b/tests/blueprints/eslint.config.mjs new file mode 100644 index 00000000000..28fded541f2 --- /dev/null +++ b/tests/blueprints/eslint.config.mjs @@ -0,0 +1,32 @@ +// @ts-check +import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; +import * as node from '@warp-drive/internal-config/eslint/node.js'; +import * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as mocha from '@warp-drive/internal-config/eslint/mocha.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + js.browser({ + srcDirs: ['fixtures'], + allowedImports: ['qunit'], + rules: { + // Fixing these would cause test failures + 'prefer-const': 'off', + 'simple-import-sort/imports': 'off', + }, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs({ + files: ['tests/*'], + }), + + mocha.cjs(), +]; diff --git a/tests/blueprints/fixtures/adapter-test/addon-default.js b/tests/blueprints/fixtures/adapter-test/addon-default.js new file mode 100644 index 00000000000..f3db94097cd --- /dev/null +++ b/tests/blueprints/fixtures/adapter-test/addon-default.js @@ -0,0 +1,12 @@ +import { setupTest } from 'dummy/tests/helpers'; +import { module, test } from 'qunit'; + +module('Unit | Adapter | foo', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + const adapter = this.owner.lookup('adapter:foo'); + assert.ok(adapter, 'adapter exists'); + }); +}); diff --git a/tests/blueprints/fixtures/adapter-test/application-default.js b/tests/blueprints/fixtures/adapter-test/application-default.js index 6dcf7317e98..f89f3a828c1 100644 --- a/tests/blueprints/fixtures/adapter-test/application-default.js +++ b/tests/blueprints/fixtures/adapter-test/application-default.js @@ -1,13 +1,12 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Adapter | application', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let adapter = this.owner.lookup('adapter:application'); - assert.ok(adapter); + const adapter = this.owner.lookup('adapter:application'); + assert.ok(adapter, 'adapter exists'); }); }); diff --git a/tests/blueprints/fixtures/adapter-test/foo-default.js b/tests/blueprints/fixtures/adapter-test/foo-default.js index 619a1ca9857..c48a37dcb73 100644 --- a/tests/blueprints/fixtures/adapter-test/foo-default.js +++ b/tests/blueprints/fixtures/adapter-test/foo-default.js @@ -1,13 +1,12 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Adapter | foo', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let adapter = this.owner.lookup('adapter:foo'); - assert.ok(adapter); + const adapter = this.owner.lookup('adapter:foo'); + assert.ok(adapter, 'adapter exists'); }); }); diff --git a/tests/blueprints/fixtures/adapter-test/foo-mocha-0.12.js b/tests/blueprints/fixtures/adapter-test/foo-mocha-0.12.js deleted file mode 100644 index 851fba84787..00000000000 --- a/tests/blueprints/fixtures/adapter-test/foo-mocha-0.12.js +++ /dev/null @@ -1,17 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'ember-mocha'; - -describe('Unit | Adapter | foo', function () { - setupTest('adapter:foo', { - // Specify the other units that are required for this test. - // needs: ['serializer:foo'] - }); - - // Replace this with your real tests. - it('exists', function () { - let adapter = this.subject(); - expect(adapter).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/adapter-test/mocha-rfc232-addon.js b/tests/blueprints/fixtures/adapter-test/mocha-rfc232-addon.js deleted file mode 100644 index 2e5fe3053da..00000000000 --- a/tests/blueprints/fixtures/adapter-test/mocha-rfc232-addon.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'dummy/tests/helpers'; - -describe('Unit | Adapter | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let adapter = this.owner.lookup('adapter:foo'); - expect(adapter).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/adapter-test/mocha-rfc232.js b/tests/blueprints/fixtures/adapter-test/mocha-rfc232.js deleted file mode 100644 index 234b0324d41..00000000000 --- a/tests/blueprints/fixtures/adapter-test/mocha-rfc232.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'my-app/tests/helpers'; - -describe('Unit | Adapter | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let adapter = this.owner.lookup('adapter:foo'); - expect(adapter).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/adapter-test/rfc232-addon.js b/tests/blueprints/fixtures/adapter-test/rfc232-addon.js deleted file mode 100644 index 4a2da233d1e..00000000000 --- a/tests/blueprints/fixtures/adapter-test/rfc232-addon.js +++ /dev/null @@ -1,13 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'dummy/tests/helpers'; - -module('Unit | Adapter | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let adapter = this.owner.lookup('adapter:foo'); - assert.ok(adapter); - }); -}); diff --git a/tests/blueprints/fixtures/adapter-test/rfc232.js b/tests/blueprints/fixtures/adapter-test/rfc232.js deleted file mode 100644 index 619a1ca9857..00000000000 --- a/tests/blueprints/fixtures/adapter-test/rfc232.js +++ /dev/null @@ -1,13 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'my-app/tests/helpers'; - -module('Unit | Adapter | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let adapter = this.owner.lookup('adapter:foo'); - assert.ok(adapter); - }); -}); diff --git a/tests/blueprints/fixtures/addon/package/package.json b/tests/blueprints/fixtures/addon/package/package.json index c35034f861e..f31b7aadb9d 100644 --- a/tests/blueprints/fixtures/addon/package/package.json +++ b/tests/blueprints/fixtures/addon/package/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "description": "Addon fixture package for ember-cli-blueprint-test-helpers", "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "devDependencies": { "ember-cli": "*", diff --git a/tests/blueprints/fixtures/app/package/package.json b/tests/blueprints/fixtures/app/package/package.json index 1a0ee71b924..2aec4f30451 100644 --- a/tests/blueprints/fixtures/app/package/package.json +++ b/tests/blueprints/fixtures/app/package/package.json @@ -4,7 +4,7 @@ "description": "App fixture package for ember-cli-blueprint-test-helpers", "private": true, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "devDependencies": { "ember-cli": "*", diff --git a/tests/blueprints/fixtures/model-test/addon-default.js b/tests/blueprints/fixtures/model-test/addon-default.js new file mode 100644 index 00000000000..9b368ceff65 --- /dev/null +++ b/tests/blueprints/fixtures/model-test/addon-default.js @@ -0,0 +1,13 @@ +import { setupTest } from 'dummy/tests/helpers'; +import { module, test } from 'qunit'; + +module('Unit | Model | foo', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + const store = this.owner.lookup('service:store'); + const model = store.createRecord('foo', {}); + assert.ok(model, 'model exists'); + }); +}); diff --git a/tests/blueprints/fixtures/model-test/comment-default.js b/tests/blueprints/fixtures/model-test/comment-default.js index 9fe885c073d..7502511fe46 100644 --- a/tests/blueprints/fixtures/model-test/comment-default.js +++ b/tests/blueprints/fixtures/model-test/comment-default.js @@ -1,14 +1,13 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Model | comment', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('comment', {}); - assert.ok(model); + const store = this.owner.lookup('service:store'); + const model = store.createRecord('comment', {}); + assert.ok(model, 'model exists'); }); }); diff --git a/tests/blueprints/fixtures/model-test/foo-default.js b/tests/blueprints/fixtures/model-test/foo-default.js index 71843632511..f15b80537d4 100644 --- a/tests/blueprints/fixtures/model-test/foo-default.js +++ b/tests/blueprints/fixtures/model-test/foo-default.js @@ -1,14 +1,13 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Model | foo', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('foo', {}); - assert.ok(model); + const store = this.owner.lookup('service:store'); + const model = store.createRecord('foo', {}); + assert.ok(model, 'model exists'); }); }); diff --git a/tests/blueprints/fixtures/model-test/foo-mocha-0.12.js b/tests/blueprints/fixtures/model-test/foo-mocha-0.12.js deleted file mode 100644 index d9aa818e125..00000000000 --- a/tests/blueprints/fixtures/model-test/foo-mocha-0.12.js +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupModelTest } from 'ember-mocha'; - -describe('Unit | Model | foo', function () { - setupModelTest('foo', { - // Specify the other units that are required for this test. - needs: [], - }); - - // Replace this with your real tests. - it('exists', function () { - let model = this.subject(); - // var store = this.store(); - expect(model).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/model-test/mocha-rfc232-addon.js b/tests/blueprints/fixtures/model-test/mocha-rfc232-addon.js deleted file mode 100644 index d2ee67d81a5..00000000000 --- a/tests/blueprints/fixtures/model-test/mocha-rfc232-addon.js +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'dummy/tests/helpers'; - -describe('Unit | Model | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('foo', {}); - expect(model).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/model-test/mocha-rfc232.js b/tests/blueprints/fixtures/model-test/mocha-rfc232.js deleted file mode 100644 index 47276f521ff..00000000000 --- a/tests/blueprints/fixtures/model-test/mocha-rfc232.js +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'my-app/tests/helpers'; - -describe('Unit | Model | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('foo', {}); - expect(model).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/model-test/post-default.js b/tests/blueprints/fixtures/model-test/post-default.js index 9a968c9dd62..a59db431ccf 100644 --- a/tests/blueprints/fixtures/model-test/post-default.js +++ b/tests/blueprints/fixtures/model-test/post-default.js @@ -1,14 +1,13 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Model | post', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('post', {}); - assert.ok(model); + const store = this.owner.lookup('service:store'); + const model = store.createRecord('post', {}); + assert.ok(model, 'model exists'); }); }); diff --git a/tests/blueprints/fixtures/model-test/rfc232-addon.js b/tests/blueprints/fixtures/model-test/rfc232-addon.js deleted file mode 100644 index eae6cef922b..00000000000 --- a/tests/blueprints/fixtures/model-test/rfc232-addon.js +++ /dev/null @@ -1,14 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'dummy/tests/helpers'; - -module('Unit | Model | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('foo', {}); - assert.ok(model); - }); -}); diff --git a/tests/blueprints/fixtures/model-test/rfc232.js b/tests/blueprints/fixtures/model-test/rfc232.js deleted file mode 100644 index 71843632511..00000000000 --- a/tests/blueprints/fixtures/model-test/rfc232.js +++ /dev/null @@ -1,14 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'my-app/tests/helpers'; - -module('Unit | Model | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('foo', {}); - assert.ok(model); - }); -}); diff --git a/tests/blueprints/fixtures/serializer-test/addon-default.js b/tests/blueprints/fixtures/serializer-test/addon-default.js new file mode 100644 index 00000000000..2ac86ce16da --- /dev/null +++ b/tests/blueprints/fixtures/serializer-test/addon-default.js @@ -0,0 +1,23 @@ +import { setupTest } from 'dummy/tests/helpers'; +import { module, test } from 'qunit'; + +module('Unit | Serializer | foo', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('foo'); + + assert.ok(serializer, 'serializer exists'); + }); + + test('it serializes records', function (assert) { + const store = this.owner.lookup('service:store'); + const record = store.createRecord('foo', {}); + + const serializedRecord = record.serialize(); + + assert.ok(serializedRecord, 'it serializes records'); + }); +}); diff --git a/tests/blueprints/fixtures/serializer-test/application-default.js b/tests/blueprints/fixtures/serializer-test/application-default.js index 64cbac10aeb..51e5be4ca2c 100644 --- a/tests/blueprints/fixtures/serializer-test/application-default.js +++ b/tests/blueprints/fixtures/serializer-test/application-default.js @@ -1,24 +1,23 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Serializer | application', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - assert.ok(serializer); + assert.ok(serializer, 'serializer exists'); }); test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('application', {}); + const store = this.owner.lookup('service:store'); + const record = store.createRecord('application', {}); - let serializedRecord = record.serialize(); + const serializedRecord = record.serialize(); - assert.ok(serializedRecord); + assert.ok(serializedRecord, 'it serializes records'); }); }); diff --git a/tests/blueprints/fixtures/serializer-test/foo-default.js b/tests/blueprints/fixtures/serializer-test/foo-default.js index da81d2da6b5..dd6ed83bd68 100644 --- a/tests/blueprints/fixtures/serializer-test/foo-default.js +++ b/tests/blueprints/fixtures/serializer-test/foo-default.js @@ -1,24 +1,23 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Serializer | foo', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('foo'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('foo'); - assert.ok(serializer); + assert.ok(serializer, 'serializer exists'); }); test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('foo', {}); + const store = this.owner.lookup('service:store'); + const record = store.createRecord('foo', {}); - let serializedRecord = record.serialize(); + const serializedRecord = record.serialize(); - assert.ok(serializedRecord); + assert.ok(serializedRecord, 'it serializes records'); }); }); diff --git a/tests/blueprints/fixtures/serializer-test/foo-mocha-0.12.js b/tests/blueprints/fixtures/serializer-test/foo-mocha-0.12.js deleted file mode 100644 index 2d1f3d0849c..00000000000 --- a/tests/blueprints/fixtures/serializer-test/foo-mocha-0.12.js +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupModelTest } from 'ember-mocha'; - -describe('Unit | Serializer | foo', function () { - setupModelTest('foo', { - // Specify the other units that are required for this test. - needs: ['serializer:foo'], - }); - - // Replace this with your real tests. - it('serializes records', function () { - let record = this.subject(); - - let serializedRecord = record.serialize(); - - expect(serializedRecord).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/serializer-test/mocha-rfc232-addon.js b/tests/blueprints/fixtures/serializer-test/mocha-rfc232-addon.js deleted file mode 100644 index 48c6a5647d8..00000000000 --- a/tests/blueprints/fixtures/serializer-test/mocha-rfc232-addon.js +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'dummy/tests/helpers'; - -describe('Unit | Serializer | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('foo'); - - expect(serializer).to.be.ok; - }); - - it('serializes records', function () { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('foo', {}); - - let serializedRecord = record.serialize(); - - expect(serializedRecord).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/serializer-test/mocha-rfc232.js b/tests/blueprints/fixtures/serializer-test/mocha-rfc232.js deleted file mode 100644 index 2141e9e620a..00000000000 --- a/tests/blueprints/fixtures/serializer-test/mocha-rfc232.js +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'my-app/tests/helpers'; - -describe('Unit | Serializer | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('foo'); - - expect(serializer).to.be.ok; - }); - - it('serializes records', function () { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('foo', {}); - - let serializedRecord = record.serialize(); - - expect(serializedRecord).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/serializer-test/rfc232-addon.js b/tests/blueprints/fixtures/serializer-test/rfc232-addon.js deleted file mode 100644 index f4e8c217a6e..00000000000 --- a/tests/blueprints/fixtures/serializer-test/rfc232-addon.js +++ /dev/null @@ -1,24 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'dummy/tests/helpers'; - -module('Unit | Serializer | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('foo'); - - assert.ok(serializer); - }); - - test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('foo', {}); - - let serializedRecord = record.serialize(); - - assert.ok(serializedRecord); - }); -}); diff --git a/tests/blueprints/fixtures/serializer-test/rfc232.js b/tests/blueprints/fixtures/serializer-test/rfc232.js deleted file mode 100644 index da81d2da6b5..00000000000 --- a/tests/blueprints/fixtures/serializer-test/rfc232.js +++ /dev/null @@ -1,24 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'my-app/tests/helpers'; - -module('Unit | Serializer | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('foo'); - - assert.ok(serializer); - }); - - test('it serializes records', function (assert) { - let store = this.owner.lookup('service:store'); - let record = store.createRecord('foo', {}); - - let serializedRecord = record.serialize(); - - assert.ok(serializedRecord); - }); -}); diff --git a/tests/blueprints/fixtures/transform-test/addon-default.js b/tests/blueprints/fixtures/transform-test/addon-default.js new file mode 100644 index 00000000000..295349e8800 --- /dev/null +++ b/tests/blueprints/fixtures/transform-test/addon-default.js @@ -0,0 +1,12 @@ +import { setupTest } from 'dummy/tests/helpers'; +import { module, test } from 'qunit'; + +module('Unit | Transform | foo', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + const transform = this.owner.lookup('transform:foo'); + assert.ok(transform, 'transform exists'); + }); +}); diff --git a/tests/blueprints/fixtures/transform-test/default.js b/tests/blueprints/fixtures/transform-test/default.js index 869294ba001..1887224dbe9 100644 --- a/tests/blueprints/fixtures/transform-test/default.js +++ b/tests/blueprints/fixtures/transform-test/default.js @@ -1,13 +1,12 @@ -import { module, test } from 'qunit'; - import { setupTest } from 'my-app/tests/helpers'; +import { module, test } from 'qunit'; module('Unit | Transform | foo', function (hooks) { setupTest(hooks); // Replace this with your real tests. test('it exists', function (assert) { - let transform = this.owner.lookup('transform:foo'); - assert.ok(transform); + const transform = this.owner.lookup('transform:foo'); + assert.ok(transform, 'transform exists'); }); }); diff --git a/tests/blueprints/fixtures/transform-test/mocha-0.12.js b/tests/blueprints/fixtures/transform-test/mocha-0.12.js deleted file mode 100644 index 2def36510ee..00000000000 --- a/tests/blueprints/fixtures/transform-test/mocha-0.12.js +++ /dev/null @@ -1,17 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'ember-mocha'; - -describe('Unit | Transform | foo', function () { - setupTest('transform:foo', { - // Specify the other units that are required for this test. - // needs: ['transform:foo'] - }); - - // Replace this with your real tests. - it('exists', function () { - let transform = this.subject(); - expect(transform).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/transform-test/mocha-rfc232-addon.js b/tests/blueprints/fixtures/transform-test/mocha-rfc232-addon.js deleted file mode 100644 index 13563b114bd..00000000000 --- a/tests/blueprints/fixtures/transform-test/mocha-rfc232-addon.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'dummy/tests/helpers'; - -describe('Unit | Transform | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let transform = this.owner.lookup('transform:foo'); - expect(transform).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/transform-test/mocha-rfc232.js b/tests/blueprints/fixtures/transform-test/mocha-rfc232.js deleted file mode 100644 index 0e5b5800153..00000000000 --- a/tests/blueprints/fixtures/transform-test/mocha-rfc232.js +++ /dev/null @@ -1,14 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { setupTest } from 'my-app/tests/helpers'; - -describe('Unit | Transform | foo', function () { - setupTest(); - - // Replace this with your real tests. - it('exists', function () { - let transform = this.owner.lookup('transform:foo'); - expect(transform).to.be.ok; - }); -}); diff --git a/tests/blueprints/fixtures/transform-test/rfc232-addon.js b/tests/blueprints/fixtures/transform-test/rfc232-addon.js deleted file mode 100644 index 9d888bbfc34..00000000000 --- a/tests/blueprints/fixtures/transform-test/rfc232-addon.js +++ /dev/null @@ -1,13 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'dummy/tests/helpers'; - -module('Unit | Transform | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let transform = this.owner.lookup('transform:foo'); - assert.ok(transform); - }); -}); diff --git a/tests/blueprints/fixtures/transform-test/rfc232.js b/tests/blueprints/fixtures/transform-test/rfc232.js deleted file mode 100644 index 869294ba001..00000000000 --- a/tests/blueprints/fixtures/transform-test/rfc232.js +++ /dev/null @@ -1,13 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'my-app/tests/helpers'; - -module('Unit | Transform | foo', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let transform = this.owner.lookup('transform:foo'); - assert.ok(transform); - }); -}); diff --git a/tests/blueprints/package.json b/tests/blueprints/package.json index e43f1293c59..5e1718cde57 100644 --- a/tests/blueprints/package.json +++ b/tests/blueprints/package.json @@ -11,15 +11,14 @@ "license": "MIT", "author": "", "scripts": { - "test": "mocha tests" + "lint": "eslint . --quiet --cache --cache-strategy=content", + "test:blueprints": "mocha tests", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependenciesMeta": { "@ember-data/unpublished-test-infra": { "injected": true }, - "@ember-data/private-build-infra": { - "injected": true - }, "@ember-data/adapter": { "injected": true }, @@ -28,33 +27,75 @@ }, "@ember-data/serializer": { "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + }, + "ember-cli-blueprint-test-helpers": { + "injected": true + }, + "ember-cli": { + "injected": true } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@ember/string": "^4.0.0", - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", + "@babel/core": "^7.24.5", + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", - "ember-cli": "~4.11.0", + "@glimmer/tracking": "^1.1.2", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-cli": "~5.12.0", "ember-cli-blueprint-test-helpers": "^0.19.2", - "ember-inflector": "^4.0.2", - "ember-source": "~4.12.0", - "mocha": "^10.2.0", - "silent-error": "^1.1.1", - "webpack": "^5.77.0" + "ember-source": "~5.12.0", + "mocha": "^10.7.3", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "silent-error": "^1.1.1" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "dependencies": {} } diff --git a/tests/blueprints/tests/adapter-test.js b/tests/blueprints/tests/adapter-test.js index 69277294be8..57a816c9f17 100644 --- a/tests/blueprints/tests/adapter-test.js +++ b/tests/blueprints/tests/adapter-test.js @@ -1,46 +1,76 @@ 'use strict'; +const { describe, it, beforeEach, afterEach } = require('mocha'); const blueprintHelpers = require('ember-cli-blueprint-test-helpers/helpers'); const chai = require('ember-cli-blueprint-test-helpers/chai'); const SilentError = require('silent-error'); -const generateFakePackageManifest = require('@ember-data/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest'); -const fixture = require('@ember-data/unpublished-test-infra/src/node-test-helpers/fixture'); -const setupTestEnvironment = require('@ember-data/unpublished-test-infra/src/node-test-helpers/setup-test-environment'); -const setupTestHooks = blueprintHelpers.setupTestHooks; +const path = require('path'); +const file = require('ember-cli-blueprint-test-helpers/chai').file; + +function fixture(directory, filePath) { + return file(path.join(directory, '../fixtures', filePath)); +} + +const { setEdition, clearEdition } = require('@ember/edition-utils'); + +function enableOctane(hooks) { + hooks.beforeEach(function () { + setEdition('octane'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function enableClassic(hooks) { + hooks.beforeEach(function () { + setEdition('classic'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + const emberNew = blueprintHelpers.emberNew; const emberGenerate = blueprintHelpers.emberGenerate; const emberGenerateDestroy = blueprintHelpers.emberGenerateDestroy; const modifyPackages = blueprintHelpers.modifyPackages; const expect = chai.expect; -const enableOctane = setupTestEnvironment.enableOctane; -const enableClassic = setupTestEnvironment.enableClassic; + +function setupTestHooks(context) { + // context.timeout = function () {}; + blueprintHelpers.setupTestHooks(context); +} describe('Acceptance: generate and destroy adapter blueprints', function () { setupTestHooks(this); describe('classic', function () { - enableClassic(); + enableClassic({ beforeEach, afterEach }); + beforeEach(async function () { await emberNew(); modifyPackages([{ name: '@ember-data/adapter', dev: true }]); }); it('adapter', async function () { - let args = ['adapter', 'foo']; + const args = ['adapter', 'foo']; await emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/foo.js')) .to.contain(`import JSONAPIAdapter from '@ember-data/adapter/json-api';`) .to.contain('export default JSONAPIAdapter.extend({'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); it('adapter extends application adapter if it exists', async function () { - let args = ['adapter', 'foo']; + const args = ['adapter', 'foo']; await emberGenerate(['adapter', 'application']); await emberGenerateDestroy(args, (_file) => { @@ -48,30 +78,31 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { .to.contain("import ApplicationAdapter from './application';") .to.contain('export default ApplicationAdapter.extend({'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); it('adapter with --base-class', async function () { - let args = ['adapter', 'foo', '--base-class=bar']; + const args = ['adapter', 'foo', '--base-class=bar']; await emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/foo.js')) .to.contain("import BarAdapter from './bar';") .to.contain('export default BarAdapter.extend({'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); + // eslint-disable-next-line mocha/no-skipped-tests xit('adapter throws when --base-class is same as name', function () { - let args = ['adapter', 'foo', '--base-class=foo']; + const args = ['adapter', 'foo', '--base-class=foo']; return expect(emberGenerate(args)).to.be.rejectedWith(SilentError, /Adapters cannot extend from themself/); }); it('adapter when is named "application"', function () { - let args = ['adapter', 'application']; + const args = ['adapter', 'application']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/application.js')) @@ -85,68 +116,16 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { }); it('adapter-test', function () { - let args = ['adapter-test', 'foo']; + const args = ['adapter-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); - }); - }); - - describe('adapter-test with ember-cli-qunit@4.1.0', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('adapter-test-test foo', function () { - return emberGenerateDestroy(['adapter-test', 'foo'], (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('adapter-test for mocha v0.12+', function () { - let args = ['adapter-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal( - fixture(__dirname, 'adapter-test/foo-mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('adapter-test for mocha v0.14+', function () { - return emberGenerateDestroy(['adapter-test', 'foo'], (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/mocha-rfc232.js')); - }); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); }); describe('octane', function () { - enableOctane(); + enableOctane({ beforeEach, afterEach }); beforeEach(async function () { await emberNew(); @@ -154,19 +133,19 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { }); it('adapter', function () { - let args = ['adapter', 'foo']; + const args = ['adapter', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/foo.js')) .to.contain(`import JSONAPIAdapter from '@ember-data/adapter/json-api';`) .to.contain('export default class FooAdapter extends JSONAPIAdapter {'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); it('adapter extends application adapter if it exists', function () { - let args = ['adapter', 'foo']; + const args = ['adapter', 'foo']; return emberGenerate(['adapter', 'application']).then(() => emberGenerateDestroy(args, (_file) => { @@ -174,25 +153,25 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { .to.contain("import ApplicationAdapter from './application';") .to.contain('export default class FooAdapter extends ApplicationAdapter {'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }) ); }); it('adapter with --base-class', function () { - let args = ['adapter', 'foo', '--base-class=bar']; + const args = ['adapter', 'foo', '--base-class=bar']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/foo.js')) .to.contain("import BarAdapter from './bar';") .to.contain('export default class FooAdapter extends BarAdapter {'); - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); it('adapter when is named "application"', function () { - let args = ['adapter', 'application']; + const args = ['adapter', 'application']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/adapters/application.js')) @@ -206,64 +185,10 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { }); it('adapter-test', function () { - let args = ['adapter-test', 'foo']; + const args = ['adapter-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232.js')); - }); - }); - - describe('adapter-test with ember-cli-qunit@4.1.0', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('adapter-test-test foo', function () { - return emberGenerateDestroy(['adapter-test', 'foo'], (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('adapter-test for mocha v0.12+', function () { - let args = ['adapter-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal( - fixture(__dirname, 'adapter-test/foo-mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('adapter-test for mocha v0.14+', function () { - let args = ['adapter-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/mocha-rfc232.js')); - }); + expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/foo-default.js')); }); }); }); @@ -275,26 +200,10 @@ describe('Acceptance: generate and destroy adapter blueprints', function () { }); describe('with ember-qunit (default)', function () { - it('adapter-test foo', function () { - return emberGenerateDestroy(['adapter-test', 'foo'], (_file) => { - expect(_file('tests/unit/adapters/foo-test.js')).to.equal(fixture(__dirname, 'adapter-test/rfc232-addon.js')); - }); - }); - }); - - describe('with ember-mocha', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.16.2'); - }); - it('adapter-test foo', function () { return emberGenerateDestroy(['adapter-test', 'foo'], (_file) => { expect(_file('tests/unit/adapters/foo-test.js')).to.equal( - fixture(__dirname, 'adapter-test/mocha-rfc232-addon.js') + fixture(__dirname, 'adapter-test/addon-default.js') ); }); }); diff --git a/tests/blueprints/tests/model-test.js b/tests/blueprints/tests/model-test.js index f77816da842..8d16bdf60ba 100644 --- a/tests/blueprints/tests/model-test.js +++ b/tests/blueprints/tests/model-test.js @@ -1,24 +1,52 @@ 'use strict'; +const { describe, it, beforeEach, afterEach } = require('mocha'); const blueprintHelpers = require('ember-cli-blueprint-test-helpers/helpers'); const chai = require('ember-cli-blueprint-test-helpers/chai'); -const generateFakePackageManifest = require('@ember-data/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest'); -const fixture = require('@ember-data/unpublished-test-infra/src/node-test-helpers/fixture'); -const setupTestEnvironment = require('@ember-data/unpublished-test-infra/src/node-test-helpers/setup-test-environment'); -const setupTestHooks = blueprintHelpers.setupTestHooks; +const path = require('path'); +const file = require('ember-cli-blueprint-test-helpers/chai').file; + +function fixture(directory, filePath) { + return file(path.join(directory, '../fixtures', filePath)); +} + const emberNew = blueprintHelpers.emberNew; const emberGenerateDestroy = blueprintHelpers.emberGenerateDestroy; const modifyPackages = blueprintHelpers.modifyPackages; const expect = chai.expect; -const enableOctane = setupTestEnvironment.enableOctane; -const enableClassic = setupTestEnvironment.enableClassic; +const { setEdition, clearEdition } = require('@ember/edition-utils'); + +function enableOctane(hooks) { + hooks.beforeEach(function () { + setEdition('octane'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function enableClassic(hooks) { + hooks.beforeEach(function () { + setEdition('classic'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function setupTestHooks(context) { + // context.timeout = function () {}; + blueprintHelpers.setupTestHooks(context); +} describe('Acceptance: generate and destroy model blueprints', function () { setupTestHooks(this); describe('classic', function () { - enableClassic(); + enableClassic({ beforeEach, afterEach }); beforeEach(async function () { await emberNew(); @@ -26,19 +54,19 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model', function () { - let args = ['model', 'foo']; + const args = ['model', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/foo.js')) .to.contain(`import Model from '@ember-data/model';`) .to.contain('export default Model.extend('); - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); it('model with attrs', function () { - let args = [ + const args = [ 'model', 'foo', 'misc', @@ -64,12 +92,12 @@ describe('Acceptance: generate and destroy model blueprints', function () { .to.contain(" name: attr('string'),") .to.contain(" customAttr: attr('custom-transform')"); - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); it('model with belongsTo', function () { - let args = ['model', 'comment', 'post:belongs-to', 'author:belongs-to:user']; + const args = ['model', 'comment', 'post:belongs-to', 'author:belongs-to:user']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/comment.js')) @@ -85,7 +113,7 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model with hasMany', function () { - let args = ['model', 'post', 'comments:has-many', 'otherComments:has-many:comment']; + const args = ['model', 'post', 'comments:has-many', 'otherComments:has-many:comment']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/post.js')) @@ -99,68 +127,16 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model-test', function () { - let args = ['model-test', 'foo']; + const args = ['model-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); - }); - }); - - describe('model-test with ember-cli-qunit@4.1.0', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('model-test-test foo', function () { - return emberGenerateDestroy(['model-test', 'foo'], (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('model-test for mocha v0.12+', function () { - let args = ['model-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-mocha-0.12.js')); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('model-test for mocha v0.14+', function () { - let args = ['model-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/mocha-rfc232.js')); - }); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); }); describe('octane', function () { - enableOctane(); + enableOctane({ beforeEach, afterEach }); beforeEach(async function () { await emberNew(); @@ -168,19 +144,19 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model', function () { - let args = ['model', 'foo']; + const args = ['model', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/foo.js')) .to.contain(`import Model from '@ember-data/model';`) .to.contain('export default class FooModel extends Model {'); - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); it('model with attrs', function () { - let args = [ + const args = [ 'model', 'foo', 'misc', @@ -206,12 +182,12 @@ describe('Acceptance: generate and destroy model blueprints', function () { .to.contain(" @attr('string') name;") .to.contain(" @attr('custom-transform') customAttr;"); - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); it('model with belongsTo', function () { - let args = ['model', 'comment', 'post:belongs-to', 'author:belongs-to:user']; + const args = ['model', 'comment', 'post:belongs-to', 'author:belongs-to:user']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/comment.js')) @@ -227,7 +203,7 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model with hasMany', function () { - let args = ['model', 'post', 'comments:has-many', 'otherComments:has-many:comment']; + const args = ['model', 'post', 'comments:has-many', 'otherComments:has-many:comment']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/models/post.js')) @@ -241,62 +217,10 @@ describe('Acceptance: generate and destroy model blueprints', function () { }); it('model-test', function () { - let args = ['model-test', 'foo']; + const args = ['model-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232.js')); - }); - }); - - describe('model-test with ember-cli-qunit@4.1.0', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('model-test-test foo', function () { - return emberGenerateDestroy(['model-test', 'foo'], (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('model-test for mocha v0.12+', function () { - let args = ['model-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-mocha-0.12.js')); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('model-test for mocha v0.14+', function () { - let args = ['model-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/mocha-rfc232.js')); - }); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/foo-default.js')); }); }); }); @@ -310,25 +234,7 @@ describe('Acceptance: generate and destroy model blueprints', function () { describe('with ember-qunit (default)', function () { it('model-test foo', function () { return emberGenerateDestroy(['model-test', 'foo'], (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/rfc232-addon.js')); - }); - }); - }); - - describe('with ember-mocha', function () { - beforeEach(function () { - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.16.2'); - }); - - it('model-test foo', function () { - return emberGenerateDestroy(['model-test', 'foo'], (_file) => { - expect(_file('tests/unit/models/foo-test.js')).to.equal( - fixture(__dirname, 'model-test/mocha-rfc232-addon.js') - ); + expect(_file('tests/unit/models/foo-test.js')).to.equal(fixture(__dirname, 'model-test/addon-default.js')); }); }); }); diff --git a/tests/blueprints/tests/serializer-test.js b/tests/blueprints/tests/serializer-test.js index 7f8c5e2b352..fa7d9e1a7bf 100644 --- a/tests/blueprints/tests/serializer-test.js +++ b/tests/blueprints/tests/serializer-test.js @@ -1,45 +1,76 @@ 'use strict'; +const { describe, it, beforeEach, afterEach } = require('mocha'); const blueprintHelpers = require('ember-cli-blueprint-test-helpers/helpers'); const chai = require('ember-cli-blueprint-test-helpers/chai'); const SilentError = require('silent-error'); -const generateFakePackageManifest = require('@ember-data/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest'); -const fixture = require('@ember-data/unpublished-test-infra/src/node-test-helpers/fixture'); -const setupTestEnvironment = require('@ember-data/unpublished-test-infra/src/node-test-helpers/setup-test-environment'); -const setupTestHooks = blueprintHelpers.setupTestHooks; +const path = require('path'); +const file = require('ember-cli-blueprint-test-helpers/chai').file; + +function fixture(directory, filePath) { + return file(path.join(directory, '../fixtures', filePath)); +} + const emberNew = blueprintHelpers.emberNew; const emberGenerate = blueprintHelpers.emberGenerate; const emberGenerateDestroy = blueprintHelpers.emberGenerateDestroy; const modifyPackages = blueprintHelpers.modifyPackages; const expect = chai.expect; -const enableOctane = setupTestEnvironment.enableOctane; -const enableClassic = setupTestEnvironment.enableClassic; +const { setEdition, clearEdition } = require('@ember/edition-utils'); + +function enableOctane(hooks) { + hooks.beforeEach(function () { + setEdition('octane'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function enableClassic(hooks) { + hooks.beforeEach(function () { + setEdition('classic'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function setupTestHooks(context) { + // context.timeout = function () {}; + blueprintHelpers.setupTestHooks(context); +} describe('Acceptance: generate and destroy serializer blueprints', function () { setupTestHooks(this); describe('classic', function () { - enableClassic(); + enableClassic({ beforeEach, afterEach }); + beforeEach(async function () { await emberNew(); await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); }); it('serializer', function () { - let args = ['serializer', 'foo']; + const args = ['serializer', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/foo.js')) .to.contain(`import JSONAPISerializer from '@ember-data/serializer/json-api';`) .to.contain('export default JSONAPISerializer.extend('); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); it('serializer extends application serializer if it exists', function () { - let args = ['serializer', 'foo']; + const args = ['serializer', 'foo']; return emberGenerate(['serializer', 'application']).then(() => emberGenerateDestroy(args, (_file) => { @@ -47,31 +78,36 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { .to.contain("import ApplicationSerializer from './application';") .to.contain('export default ApplicationSerializer.extend({'); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }) ); }); it('serializer with --base-class', function () { - let args = ['serializer', 'foo', '--base-class=bar']; + const args = ['serializer', 'foo', '--base-class=bar']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/foo.js')) .to.contain("import BarSerializer from './bar';") .to.contain('export default BarSerializer.extend({'); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); + // eslint-disable-next-line mocha/no-skipped-tests xit('serializer throws when --base-class is same as name', function () { - let args = ['serializer', 'foo', '--base-class=foo']; + const args = ['serializer', 'foo', '--base-class=foo']; return expect(emberGenerate(args)).to.be.rejectedWith(SilentError, /Serializers cannot extend from themself/); }); it('serializer when is named "application"', function () { - let args = ['serializer', 'application']; + const args = ['serializer', 'application']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/application.js')) @@ -85,77 +121,18 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { }); it('serializer-test', function () { - let args = ['serializer-test', 'foo']; + const args = ['serializer-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); - }); - }); - - describe('serializer-test with ember-cli-qunit@4.1.0', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - await modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('serializer-test-test foo', function () { - return emberGenerateDestroy(['serializer-test', 'foo'], (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/foo-default.js') - ); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('serializer-test for mocha v0.12+', function () { - let args = ['serializer-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/foo-mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - await modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('serializer-test for mocha v0.14+', function () { - let args = ['serializer-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/mocha-rfc232.js') - ); - }); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); }); describe('octane', function () { - enableOctane(); + enableOctane({ beforeEach, afterEach }); beforeEach(async function () { await emberNew(); @@ -163,19 +140,21 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { }); it('serializer', function () { - let args = ['serializer', 'foo']; + const args = ['serializer', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/foo.js')) .to.contain(`import JSONAPISerializer from '@ember-data/serializer/json-api';`) .to.contain('export default class FooSerializer extends JSONAPISerializer {'); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); it('serializer extends application serializer if it exists', function () { - let args = ['serializer', 'foo']; + const args = ['serializer', 'foo']; return emberGenerate(['serializer', 'application']).then(() => emberGenerateDestroy(args, (_file) => { @@ -183,31 +162,36 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { .to.contain("import ApplicationSerializer from './application';") .to.contain('export default class FooSerializer extends ApplicationSerializer {'); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }) ); }); it('serializer with --base-class', function () { - let args = ['serializer', 'foo', '--base-class=bar']; + const args = ['serializer', 'foo', '--base-class=bar']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/foo.js')) .to.contain("import BarSerializer from './bar';") .to.contain('export default class FooSerializer extends BarSerializer'); - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); + // eslint-disable-next-line mocha/no-skipped-tests xit('serializer throws when --base-class is same as name', function () { - let args = ['serializer', 'foo', '--base-class=foo']; + const args = ['serializer', 'foo', '--base-class=foo']; return expect(emberGenerate(args)).to.be.rejectedWith(SilentError, /Serializers cannot extend from themself/); }); it('serializer when is named "application"', function () { - let args = ['serializer', 'application']; + const args = ['serializer', 'application']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/serializers/application.js')) @@ -221,71 +205,12 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { }); it('serializer-test', function () { - let args = ['serializer-test', 'foo']; + const args = ['serializer-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal(fixture(__dirname, 'serializer-test/rfc232.js')); - }); - }); - - describe('serializer-test with ember-cli-qunit@4.1.0', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('serializer-test-test foo', function () { - return emberGenerateDestroy(['serializer-test', 'foo'], (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/foo-default.js') - ); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('serializer-test for mocha v0.12+', function () { - let args = ['serializer-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/foo-mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('serializer-test for mocha v0.14+', function () { - let args = ['serializer-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/mocha-rfc232.js') - ); - }); + expect(_file('tests/unit/serializers/foo-test.js')).to.equal( + fixture(__dirname, 'serializer-test/foo-default.js') + ); }); }); }); @@ -300,26 +225,7 @@ describe('Acceptance: generate and destroy serializer blueprints', function () { it('serializer-test foo', function () { return emberGenerateDestroy(['serializer-test', 'foo'], (_file) => { expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/rfc232-addon.js') - ); - }); - }); - }); - - describe('with ember-mocha', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.16.2'); - }); - - it('serializer-test foo', function () { - return emberGenerateDestroy(['serializer-test', 'foo'], (_file) => { - expect(_file('tests/unit/serializers/foo-test.js')).to.equal( - fixture(__dirname, 'serializer-test/mocha-rfc232-addon.js') + fixture(__dirname, 'serializer-test/addon-default.js') ); }); }); diff --git a/tests/blueprints/tests/transform-test.js b/tests/blueprints/tests/transform-test.js index e83ccc3d012..75ce0f09b77 100644 --- a/tests/blueprints/tests/transform-test.js +++ b/tests/blueprints/tests/transform-test.js @@ -1,24 +1,53 @@ 'use strict'; +const { describe, it, beforeEach, afterEach } = require('mocha'); const blueprintHelpers = require('ember-cli-blueprint-test-helpers/helpers'); const chai = require('ember-cli-blueprint-test-helpers/chai'); -const generateFakePackageManifest = require('@ember-data/unpublished-test-infra/src/node-test-helpers/generate-fake-package-manifest'); -const fixture = require('@ember-data/unpublished-test-infra/src/node-test-helpers/fixture'); -const setupTestEnvironment = require('@ember-data/unpublished-test-infra/src/node-test-helpers/setup-test-environment'); -const setupTestHooks = blueprintHelpers.setupTestHooks; +const path = require('path'); +const file = require('ember-cli-blueprint-test-helpers/chai').file; + +function fixture(directory, filePath) { + return file(path.join(directory, '../fixtures', filePath)); +} + const emberNew = blueprintHelpers.emberNew; const emberGenerateDestroy = blueprintHelpers.emberGenerateDestroy; const modifyPackages = blueprintHelpers.modifyPackages; const expect = chai.expect; -const enableOctane = setupTestEnvironment.enableOctane; -const enableClassic = setupTestEnvironment.enableClassic; +const { setEdition, clearEdition } = require('@ember/edition-utils'); + +function enableOctane(hooks) { + hooks.beforeEach(function () { + setEdition('octane'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function enableClassic(hooks) { + hooks.beforeEach(function () { + setEdition('classic'); + }); + + hooks.afterEach(function () { + clearEdition(); + }); +} + +function setupTestHooks(context) { + // context.timeout = function () {}; + blueprintHelpers.setupTestHooks(context); +} describe('Acceptance: generate and destroy transform blueprints', function () { setupTestHooks(this); describe('classic', function () { - enableClassic(); + enableClassic({ beforeEach, afterEach }); + describe('in app', function () { beforeEach(async function () { await emberNew(); @@ -26,7 +55,7 @@ describe('Acceptance: generate and destroy transform blueprints', function () { }); it('transform', function () { - let args = ['transform', 'foo']; + const args = ['transform', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/transforms/foo.js')) @@ -34,76 +63,15 @@ describe('Acceptance: generate and destroy transform blueprints', function () { .to.contain('deserialize(serialized) {') .to.contain('serialize(deserialized) {'); - expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/rfc232.js')); + expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/default.js')); }); }); it('transform-test', function () { - let args = ['transform-test', 'foo']; + const args = ['transform-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/rfc232.js')); - }); - }); - - describe('transform-test with ember-cli-qunit@4.1.0', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('transform-test-test foo', function () { - return emberGenerateDestroy(['transform-test', 'foo'], (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/default.js') - ); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('transform-test for mocha v0.12+', function () { - let args = ['transform-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('transform-test for mocha v0.14+', function () { - let args = ['transform-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/mocha-rfc232.js') - ); - }); + expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/default.js')); }); }); }); @@ -111,7 +79,7 @@ describe('Acceptance: generate and destroy transform blueprints', function () { describe('octane', function () { describe('in app', function () { - enableOctane(); + enableOctane({ beforeEach, afterEach }); beforeEach(async function () { await emberNew(); @@ -119,7 +87,7 @@ describe('Acceptance: generate and destroy transform blueprints', function () { }); it('transform', function () { - let args = ['transform', 'foo']; + const args = ['transform', 'foo']; return emberGenerateDestroy(args, (_file) => { expect(_file('app/transforms/foo.js')) @@ -127,76 +95,15 @@ describe('Acceptance: generate and destroy transform blueprints', function () { .to.contain('deserialize(serialized) {') .to.contain('serialize(deserialized) {'); - expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/rfc232.js')); + expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/default.js')); }); }); it('transform-test', function () { - let args = ['transform-test', 'foo']; + const args = ['transform-test', 'foo']; return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/rfc232.js')); - }); - }); - - describe('transform-test with ember-cli-qunit@4.1.0', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-qunit', delete: true }, - ]); - generateFakePackageManifest('ember-cli-qunit', '4.1.0'); - }); - - it('transform-test-test foo', function () { - return emberGenerateDestroy(['transform-test', 'foo'], (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/default.js') - ); - }); - }); - }); - - describe('with ember-cli-mocha v0.12+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-cli-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-cli-mocha', '0.12.0'); - }); - - it('transform-test for mocha v0.12+', function () { - let args = ['transform-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/mocha-0.12.js') - ); - }); - }); - }); - - describe('with ember-mocha v0.14+', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.14.0'); - }); - - it('transform-test for mocha v0.14+', function () { - let args = ['transform-test', 'foo']; - - return emberGenerateDestroy(args, (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/mocha-rfc232.js') - ); - }); + expect(_file('tests/unit/transforms/foo-test.js')).to.equal(fixture(__dirname, 'transform-test/default.js')); }); }); }); @@ -212,26 +119,7 @@ describe('Acceptance: generate and destroy transform blueprints', function () { it('transform-test foo', function () { return emberGenerateDestroy(['transform-test', 'foo'], (_file) => { expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/rfc232-addon.js') - ); - }); - }); - }); - - describe('with ember-mocha', function () { - beforeEach(async function () { - await modifyPackages([{ name: '@ember-data/serializer', dev: true }]); - modifyPackages([ - { name: 'ember-qunit', delete: true }, - { name: 'ember-mocha', dev: true }, - ]); - generateFakePackageManifest('ember-mocha', '0.16.2'); - }); - - it('transform-test foo', function () { - return emberGenerateDestroy(['transform-test', 'foo'], (_file) => { - expect(_file('tests/unit/transforms/foo-test.js')).to.equal( - fixture(__dirname, 'transform-test/mocha-rfc232-addon.js') + fixture(__dirname, 'transform-test/addon-default.js') ); }); }); diff --git a/tests/builders/README.md b/tests/builders/README.md new file mode 100644 index 00000000000..d506f5d27d5 --- /dev/null +++ b/tests/builders/README.md @@ -0,0 +1,8 @@ +# Builder Tests + +Provides testing for the Request and URL Building Utils + +- @ember-data/active-record/request +- @ember-data/json-api/request +- @ember-data/rest/request +- @ember-data/request-utils diff --git a/tests/builders/app/app.ts b/tests/builders/app/app.ts new file mode 100644 index 00000000000..8f235d166e6 --- /dev/null +++ b/tests/builders/app/app.ts @@ -0,0 +1,16 @@ +import Application from '@ember/application'; + +import loadInitializers from 'ember-load-initializers'; + +import config from './config/environment'; +import Resolver from './resolver'; + +class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + override Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/tests/graph/app/config/environment.d.ts b/tests/builders/app/config/environment.d.ts similarity index 100% rename from tests/graph/app/config/environment.d.ts rename to tests/builders/app/config/environment.d.ts diff --git a/tests/builders/app/index.html b/tests/builders/app/index.html new file mode 100644 index 00000000000..15be5ed9d19 --- /dev/null +++ b/tests/builders/app/index.html @@ -0,0 +1,25 @@ + + + + + + EmberData Request Builders Test App + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/builders/app/models/user-setting.ts b/tests/builders/app/models/user-setting.ts new file mode 100644 index 00000000000..e8f3511d0a0 --- /dev/null +++ b/tests/builders/app/models/user-setting.ts @@ -0,0 +1,5 @@ +import Model, { attr } from '@ember-data/model'; + +export default class UserSetting extends Model { + @attr declare name: string; +} diff --git a/tests/graph/app/resolver.ts b/tests/builders/app/resolver.ts similarity index 100% rename from tests/graph/app/resolver.ts rename to tests/builders/app/resolver.ts diff --git a/tests/graph/app/router.ts b/tests/builders/app/router.ts similarity index 100% rename from tests/graph/app/router.ts rename to tests/builders/app/router.ts diff --git a/tests/builders/app/services/store.ts b/tests/builders/app/services/store.ts new file mode 100644 index 00000000000..0b3c4821783 --- /dev/null +++ b/tests/builders/app/services/store.ts @@ -0,0 +1,46 @@ +import JSONAPICache from '@ember-data/json-api'; +import type Model from '@ember-data/model'; +import { instantiateRecord, teardownRecord } from '@ember-data/model'; +import { buildSchema, modelFor } from '@ember-data/model/hooks'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import DataStore, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; + +export default class Store extends DataStore { + constructor(args: unknown) { + super(args); + + const manager = (this.requestManager = new RequestManager()); + manager.use([Fetch]); + manager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new JSONAPICache(capabilities); + } + + override instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): unknown { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model): void { + return teardownRecord.call(this, record); + } + + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { + return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type); + } +} diff --git a/tests/adapter-encapsulation/app/styles/app.css b/tests/builders/app/styles/app.css similarity index 100% rename from tests/adapter-encapsulation/app/styles/app.css rename to tests/builders/app/styles/app.css diff --git a/tests/adapter-encapsulation/app/controllers/.gitkeep b/tests/builders/app/templates/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/controllers/.gitkeep rename to tests/builders/app/templates/.gitkeep diff --git a/tests/builders/app/templates/application.hbs b/tests/builders/app/templates/application.hbs new file mode 100644 index 00000000000..c24cd68950a --- /dev/null +++ b/tests/builders/app/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/tests/builders/config/environment.js b/tests/builders/config/environment.js new file mode 100644 index 00000000000..08309c1f849 --- /dev/null +++ b/tests/builders/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'builders-test-app', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/packages/unpublished-test-infra/tests/dummy/config/optional-features.json b/tests/builders/config/optional-features.json similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/config/optional-features.json rename to tests/builders/config/optional-features.json diff --git a/packages/unpublished-test-infra/tests/dummy/config/targets.js b/tests/builders/config/targets.js similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/config/targets.js rename to tests/builders/config/targets.js diff --git a/tests/builders/diagnostic.js b/tests/builders/diagnostic.js new file mode 100644 index 00000000000..ede75dbb1ad --- /dev/null +++ b/tests/builders/diagnostic.js @@ -0,0 +1,3 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; + +await launch(); diff --git a/tests/builders/ember-cli-build.js b/tests/builders/ember-cli-build.js new file mode 100644 index 00000000000..4b20eb3c50e --- /dev/null +++ b/tests/builders/ember-cli-build.js @@ -0,0 +1,38 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + // autoImport: { + // forbidEval: true, + // webpack: { + // devtool: 'source-map', + // optimization: { + // minimize: false, + // moduleIds: 'named', + // }, + // }, + // }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); + + return app.toTree(); +}; diff --git a/tests/builders/eslint.config.mjs b/tests/builders/eslint.config.mjs new file mode 100644 index 00000000000..5e2079d612b --- /dev/null +++ b/tests/builders/eslint.config.mjs @@ -0,0 +1,28 @@ +// @ts-check +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 * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // browser (test) ================ + diagnostic.browser({ + allowedImports: ['ember-inflector'], + }), +]; diff --git a/tests/builders/package.json b/tests/builders/package.json new file mode 100644 index 00000000000..d6373c3874b --- /dev/null +++ b/tests/builders/package.json @@ -0,0 +1,131 @@ +{ + "name": "builders-test-app", + "version": "4.12.8", + "private": true, + "description": "Provides tests for URL and Request Building Capabilities", + "keywords": [], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "tests/builders" + }, + "license": "MIT", + "author": "", + "directories": { + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "test": "bun ./diagnostic.js", + "test:production": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/rest": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/active-record": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@warp-drive/diagnostic": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/active-record": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/rest": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^3.1.1", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.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.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-test-loader": "^3.1.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-inflector": "^4.0.3", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "ember-source-channel-url": "^3.0.0", + "loader.js": "^4.7.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "silent-error": "^1.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "ember": { + "edition": "octane" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": {} +} diff --git a/tests/adapter-encapsulation/app/helpers/.gitkeep b/tests/builders/tests/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/helpers/.gitkeep rename to tests/builders/tests/.gitkeep diff --git a/tests/builders/tests/helpers/utils.ts b/tests/builders/tests/helpers/utils.ts new file mode 100644 index 00000000000..3c2163ea19f --- /dev/null +++ b/tests/builders/tests/helpers/utils.ts @@ -0,0 +1,7 @@ +export function headersToObject(headers: Headers) { + const result: { [key: string]: string } = {}; + headers.forEach((value, key) => { + result[key] = value; + }); + return result; +} diff --git a/tests/builders/tests/index.html b/tests/builders/tests/index.html new file mode 100644 index 00000000000..644b3bd1094 --- /dev/null +++ b/tests/builders/tests/index.html @@ -0,0 +1,37 @@ + + + + + + Request Builder Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+ + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + + diff --git a/tests/builders/tests/integration/create-record-test.ts b/tests/builders/tests/integration/create-record-test.ts new file mode 100644 index 00000000000..74049fc87e9 --- /dev/null +++ b/tests/builders/tests/integration/create-record-test.ts @@ -0,0 +1,264 @@ +import type { TestContext } from '@ember/test-helpers'; + +import JSONAPICache from '@ember-data/json-api'; +import { createRecord } from '@ember-data/json-api/request'; +import Model, { attr, instantiateRecord, teardownRecord } from '@ember-data/model'; +import { buildSchema, modelFor } from '@ember-data/model/hooks'; +import type { Future, Handler, RequestContext, StructuredDataDocument } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import DataStore, { CacheHandler, recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class TestStore extends DataStore { + constructor(args: unknown) { + super(args); + + const manager = (this.requestManager = new RequestManager()); + manager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new JSONAPICache(capabilities); + } + + override instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): unknown { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model): void { + return teardownRecord.call(this, record); + } + + override modelFor(type: string): ModelSchema { + return modelFor.call(this, type) as ModelSchema; + } +} + +class User extends Model { + @attr declare name: string; +} + +module('Integration - createRecord', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('Saving a new record with a createRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + // eslint-disable-next-line prefer-const + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.createRecord('user', { name: 'John' }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.true(user.isNew, 'The user is new'); + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + + response = { + data: { + id: '1', + type: 'user', + attributes: { + name: 'Chris', + }, + }, + }; + + const promise = store.request(createRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + await promise; + + assert.equal(user.id, '1', 'The user is updated from the response'); + assert.equal(user.name, 'Chris', 'The user is updated from the response'); + assert.false(user.hasDirtyAttributes, 'The user is no longer dirty'); + assert.false(user.isNew, 'The user is no longer new'); + assert.false(user.isSaving, 'The user is no longer saving'); + + assert.verifySteps([`willCommit ${identifier.lid}`, 'handle createRecord request', `didCommit ${identifier.lid}`]); + }); + + test('Rejecting during Save of a new record with a createRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + // eslint-disable-next-line prefer-const + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.createRecord('user', { name: 'John' }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.true(user.isNew, 'The user is new'); + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + + const validationError: Error & { + content: { errors: ApiError[] }; + } = new Error('Something went wrong') as Error & { + content: { errors: ApiError[] }; + }; + validationError.content = { + errors: [ + { + title: 'Name must be capitalized', + detail: 'Name must be capitalized', + source: { + pointer: '/data/attributes/name', + }, + }, + ], + }; + + response = validationError; + + const promise = store.request(createRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + try { + await promise; + assert.ok(false, 'The promise should reject'); + } catch (e: unknown) { + assert.true(e instanceof Error, 'The error is an error'); + assert.equal((e as Error).message, 'Something went wrong', 'The error has the expected error message'); + assert.true( + Array.isArray((e as { content: { errors: ApiError[] } })?.content?.errors), + 'The error has an errors array' + ); + } + + assert.equal(user.id, null, 'The user is not updated from the response'); + assert.equal(user.name, 'John', 'The user is not updated from the response'); + assert.true(user.hasDirtyAttributes, 'The user is still dirty'); + assert.true(user.isNew, 'The user is still new'); + assert.false(user.isDeleted, 'The user is not deleted'); + assert.false(user.isDestroying, 'The user is not destroying'); + assert.false(user.isDestroyed, 'The user is not destroyed'); + assert.false(user.isSaving, 'The user is no longer saving'); + + // TODO: Errors type is missing `get` + + const nameErrors = user.errors.get('name') as Array<{ + attribute: string; + message: string; + }>; + + assert.equal(nameErrors.length, 1, 'The user has the expected number of errors'); + assert.equal(nameErrors[0]?.message, 'Name must be capitalized', 'The user has the expected error for the field'); + + assert.verifySteps([ + `willCommit ${identifier.lid}`, + 'handle createRecord request', + `commitWasRejected ${identifier.lid}`, + ]); + }); +}); diff --git a/tests/builders/tests/integration/delete-record-test.ts b/tests/builders/tests/integration/delete-record-test.ts new file mode 100644 index 00000000000..cb90491a5ed --- /dev/null +++ b/tests/builders/tests/integration/delete-record-test.ts @@ -0,0 +1,295 @@ +import type { TestContext } from '@ember/test-helpers'; + +import JSONAPICache from '@ember-data/json-api'; +import { deleteRecord } from '@ember-data/json-api/request'; +import Model, { attr, instantiateRecord, teardownRecord } from '@ember-data/model'; +import { buildSchema, modelFor } from '@ember-data/model/hooks'; +import type { Future, Handler, RequestContext, StructuredDataDocument } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import DataStore, { CacheHandler, recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class TestStore extends DataStore { + constructor(args: unknown) { + super(args); + + const manager = (this.requestManager = new RequestManager()); + manager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new JSONAPICache(capabilities); + } + + override instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): unknown { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model): void { + return teardownRecord.call(this, record); + } + + override modelFor(type: string): ModelSchema { + return modelFor.call(this, type) as ModelSchema; + } +} + +class User extends Model { + @attr declare name: string; +} + +module('Integration - deleteRecord', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('Persisting deletion for a record with a deleteRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + // eslint-disable-next-line prefer-const + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' } } }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.false(user.isDeleted, 'The user is not deleted'); + assert.false(user.hasDirtyAttributes, 'The user is not dirty'); + + // our delete response will include some sideloaded data + // to ensure it is properly handled + response = { + included: [ + { + id: '2', + type: 'user', + attributes: { + name: 'John', + }, + }, + ], + }; + + store.deleteRecord(user); + + assert.true(user.isDeleted, 'The user is deleted'); + assert.false(user.isSaving, 'The user is not saving'); + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + assert.equal(user.currentState.stateName, 'root.deleted.uncommitted', 'The user is in the correct state'); + assert.equal(user.dirtyType, 'deleted', 'The user is dirty with the correct type'); + + const promise = store.request(deleteRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + await promise; + + assert.false(user.hasDirtyAttributes, 'The user is not dirty'); + assert.equal(user.currentState.stateName, 'root.deleted.saved', 'The user is in the correct state'); + assert.equal(user.dirtyType, '', 'The user is no longer dirty'); + assert.true(user.isDeleted, 'The user is deleted'); + assert.false(user.isSaving, 'The user is no longer saving'); + + assert.verifySteps([`willCommit ${identifier.lid}`, 'handle deleteRecord request', `didCommit ${identifier.lid}`]); + + const user2 = store.peekRecord('user', '2') as User; + assert.notEqual(user2, null, 'The user is in the store'); + assert.equal(user2?.name, 'John', 'The user has the expected name'); + }); + + test('Rejecting while persisting a deletion with a deleteRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' } } }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.false(user.isDeleted, 'The user is not deleted'); + assert.false(user.hasDirtyAttributes, 'The user is not dirty'); + + // our delete response will include some sideloaded data + // to ensure it is properly handled + response = { + included: [ + { + id: '2', + type: 'user', + attributes: { + name: 'John', + }, + }, + ], + }; + + store.deleteRecord(user); + + assert.true(user.isDeleted, 'The user is deleted'); + assert.false(user.isSaving, 'The user is not saving'); + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + assert.equal(user.currentState.stateName, 'root.deleted.uncommitted', 'The user is in the correct state'); + assert.equal(user.dirtyType, 'deleted', 'The user is dirty with the correct type'); + + const validationError: Error & { + content: { errors: ApiError[] }; + } = new Error('405 | Not Authorized') as Error & { + content: { errors: ApiError[] }; + }; + validationError.content = { + errors: [ + { + title: 'Not Authorized', + detail: 'Not Authorized', + source: { + pointer: '/data', + }, + }, + ], + }; + + response = validationError; + + const promise = store.request(deleteRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + try { + await promise; + assert.ok(false, 'The promise should reject'); + } catch (e: unknown) { + assert.true(e instanceof Error, 'The error is an error'); + assert.equal((e as Error).message, '405 | Not Authorized', 'The error has the expected error message'); + assert.true( + Array.isArray((e as { content: { errors: ApiError[] } })?.content?.errors), + 'The error has an errors array' + ); + } + + assert.false(user.isDestroying, 'The user is not destroying'); + assert.false(user.isDestroyed, 'The user is not destroyed'); + assert.true(user.hasDirtyAttributes, 'The user is still dirty'); + assert.equal(user.currentState.stateName, 'root.deleted.invalid', 'The user is in the correct state'); + assert.equal(user.dirtyType, 'deleted', 'The user is still dirty'); + assert.equal( + (user.adapterError as Error)?.message, + '405 | Not Authorized', + 'The user has the expected error message' + ); + assert.true(user.isDeleted, 'The user is still deleted'); + assert.false(user.isSaving, 'The user is no longer saving'); + + assert.verifySteps([ + `willCommit ${identifier.lid}`, + 'handle deleteRecord request', + `commitWasRejected ${identifier.lid}`, + ]); + }); +}); diff --git a/tests/builders/tests/integration/update-record-test.ts b/tests/builders/tests/integration/update-record-test.ts new file mode 100644 index 00000000000..98acf038590 --- /dev/null +++ b/tests/builders/tests/integration/update-record-test.ts @@ -0,0 +1,266 @@ +import type { TestContext } from '@ember/test-helpers'; + +import JSONAPICache from '@ember-data/json-api'; +import { updateRecord } from '@ember-data/json-api/request'; +import Model, { attr, instantiateRecord, teardownRecord } from '@ember-data/model'; +import { buildSchema, modelFor } from '@ember-data/model/hooks'; +import type { Future, Handler, RequestContext, StructuredDataDocument } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import DataStore, { CacheHandler, recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class TestStore extends DataStore { + constructor(args: unknown) { + super(args); + + const manager = (this.requestManager = new RequestManager()); + manager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new JSONAPICache(capabilities); + } + + override instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): unknown { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model): void { + return teardownRecord.call(this, record); + } + + override modelFor(type: string): ModelSchema { + return modelFor.call(this, type) as ModelSchema; + } +} + +class User extends Model { + @attr declare name: string; +} + +module('Integration - updateRecord', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('Saving a record with an updateRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + // eslint-disable-next-line prefer-const + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' } } }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.false(user.isNew, 'The user is not new'); + assert.false(user.hasDirtyAttributes, 'The user is not dirty'); + + response = { + data: { + id: '1', + type: 'user', + attributes: { + name: 'James Thoburn', + }, + }, + }; + + user.name = 'James'; + + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + const promise = store.request(updateRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + await promise; + + assert.equal(user.name, 'James Thoburn', 'The user is updated from the response'); + assert.false(user.hasDirtyAttributes, 'The user is no longer dirty'); + assert.false(user.isSaving, 'The user is no longer saving'); + + assert.verifySteps([`willCommit ${identifier.lid}`, 'handle updateRecord request', `didCommit ${identifier.lid}`]); + }); + + test('Rejecting during Save of a new record with a createRecord op works as expected', async function (this: TestContext, assert) { + const { owner } = this; + + // intercept cache APIs to ensure they are called as expected + class TestCache extends JSONAPICache { + override willCommit(identifier: StableRecordIdentifier): void { + assert.step(`willCommit ${identifier.lid}`); + return super.willCommit(identifier); + } + override didCommit( + committedIdentifier: StableRecordIdentifier, + result: StructuredDataDocument + ): SingleResourceDataDocument { + assert.step(`didCommit ${committedIdentifier.lid}`); + return super.didCommit(committedIdentifier, result); + } + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + assert.step(`commitWasRejected ${identifier.lid}`); + return super.commitWasRejected(identifier, errors); + } + } + + // intercept Handler APIs to ensure they are called as expected + // eslint-disable-next-line prefer-const + let response: unknown; + const TestHandler: Handler = { + request(context: RequestContext): Promise> | Future { + assert.step(`handle ${context.request.op} request`); + assert.ok(response, 'response is set'); + + if (response instanceof Error) { + throw response; + } + return Promise.resolve(response as T); + }, + }; + + class Store extends TestStore { + constructor(args: unknown) { + super(args); + const manager = this.requestManager; + manager.use([TestHandler]); + } + override createCache(capabilities: CacheCapabilitiesManager): Cache { + return new TestCache(capabilities); + } + } + + owner.register('service:store', Store); + owner.register('model:user', User); + const store = owner.lookup('service:store') as Store; + const user = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' } } }) as User; + const identifier = recordIdentifierFor(user); + assert.false(user.isSaving, 'The user is not saving'); + assert.false(user.isNew, 'The user is not new'); + assert.false(user.hasDirtyAttributes, 'The user is not dirty'); + + const validationError: Error & { + content: { errors: ApiError[] }; + } = new Error('Something went wrong') as Error & { + content: { errors: ApiError[] }; + }; + validationError.content = { + errors: [ + { + title: 'Name must be capitalized', + detail: 'Name must be capitalized', + source: { + pointer: '/data/attributes/name', + }, + }, + ], + }; + + response = validationError; + + user.name = 'james'; + assert.true(user.hasDirtyAttributes, 'The user is dirty'); + + const promise = store.request(updateRecord(user)); + assert.true(user.isSaving, 'The user is saving'); + + try { + await promise; + assert.ok(false, 'The promise should reject'); + } catch (e: unknown) { + assert.true(e instanceof Error, 'The error is an error'); + assert.equal((e as Error).message, 'Something went wrong', 'The error has the expected error message'); + assert.true( + Array.isArray((e as { content: { errors: ApiError[] } })?.content?.errors), + 'The error has an errors array' + ); + } + + assert.true(user.hasDirtyAttributes, 'The user is still dirty'); + assert.false(user.isNew, 'The user is not new'); + assert.false(user.isDeleted, 'The user is not deleted'); + assert.false(user.isDestroying, 'The user is not destroying'); + assert.false(user.isDestroyed, 'The user is not destroyed'); + assert.false(user.isSaving, 'The user is no longer saving'); + + // TODO: Errors type is missing `get` + + const nameErrors = user.errors.get('name') as Array<{ + attribute: string; + message: string; + }>; + + assert.equal(nameErrors.length, 1, 'The user has the expected number of errors'); + assert.equal(nameErrors[0]?.message, 'Name must be capitalized', 'The user has the expected error for the field'); + + assert.verifySteps([ + `willCommit ${identifier.lid}`, + 'handle updateRecord request', + `commitWasRejected ${identifier.lid}`, + ]); + }); +}); diff --git a/tests/builders/tests/test-helper.js b/tests/builders/tests/test-helper.js new file mode 100644 index 00000000000..7469a0ee78b --- /dev/null +++ b/tests/builders/tests/test-helper.js @@ -0,0 +1,25 @@ +import { setApplication } from '@ember/test-helpers'; + +import configureAsserts from '@ember-data/unpublished-test-infra/test-support/asserts/index'; +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; + +import Application from '../app'; +import config from '../config/environment'; + +configure(); + +setupGlobalHooks((hooks) => { + configureAsserts(hooks); +}); + +setApplication(Application.create(config.APP)); +start({ + tryCatch: false, + debug: false, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/builders/tests/unit/active-record-builder-test.ts b/tests/builders/tests/unit/active-record-builder-test.ts new file mode 100644 index 00000000000..a1498c7b461 --- /dev/null +++ b/tests/builders/tests/unit/active-record-builder-test.ts @@ -0,0 +1,297 @@ +import type { TestContext } from '@ember/test-helpers'; + +import { createRecord, deleteRecord, findRecord, query, updateRecord } from '@ember-data/active-record/request'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +import type UserSetting from '../../app/models/user-setting'; +import { headersToObject } from '../helpers/utils'; + +const ACTIVE_RECORD_HEADERS = { accept: 'application/json;charset=utf-8' }; + +module('ActiveRecord | Request Builders', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('findRecord by identifier', function (this: TestContext, assert) { + const result = findRecord({ type: 'user-setting', id: '1' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/1', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('findRecord by type+id', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1'); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/1', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('findRecord by identifier with options', function (this: TestContext, assert) { + const result = findRecord( + { type: 'user-setting', id: '1' }, + { reload: true, backgroundReload: false, include: ['user', 'friends'] } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: { + reload: true, + backgroundReload: false, + }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier and options` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('findRecord by type+id with options', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1', { reload: true, backgroundReload: false, include: 'user,friends' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id and options` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('query', function (this: TestContext, assert) { + const result = query( + 'user-setting', + { include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }, + { reload: true, backgroundReload: false } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and options` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('query with empty params [used to be findAll]', function (this: TestContext, assert) { + const result = query('user-setting', {}, { reload: true, backgroundReload: false }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings', + method: 'GET', + headers: new Headers(ACTIVE_RECORD_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and empty options, does not leave a trailing ?` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS); + }); + + test('createRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings', + method: 'POST', + headers: new Headers(ACTIVE_RECORD_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS, "headers are set to ActiveRecord API's"); + }); + + test('createRecord passing store record and options', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting, { resourcePath: 'user-settings/new' }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/new', + method: 'POST', + headers: new Headers(ACTIVE_RECORD_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS, "headers are set to ActiveRecord API's"); + }); + + test('updateRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/12', + method: 'PUT', + headers: new Headers(ACTIVE_RECORD_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS, "headers are set to ActiveRecord API's"); + }); + + test('updateRecord with PATCH method', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting, { patch: true }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/12', + method: 'PATCH', + headers: new Headers(ACTIVE_RECORD_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS, "headers are set to ActiveRecord API's"); + }); + + test('deleteRecord with identifier', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12'); + const identifier = recordIdentifierFor(userSetting); + + const result = deleteRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user_settings/12', + method: 'DELETE', + headers: new Headers(ACTIVE_RECORD_HEADERS), + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `deleteRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), ACTIVE_RECORD_HEADERS, "headers are set to ActiveRecord API's"); + }); +}); diff --git a/tests/builders/tests/unit/build-base-url-test.ts b/tests/builders/tests/unit/build-base-url-test.ts new file mode 100644 index 00000000000..bd27f78dd5a --- /dev/null +++ b/tests/builders/tests/unit/build-base-url-test.ts @@ -0,0 +1,289 @@ +import { buildBaseURL, setBuildURLConfig } from '@ember-data/request-utils'; +import { test as debug } from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { module, test } from '@warp-drive/diagnostic'; + +module('buildBaseURL', function (hooks) { + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('simple cases (no optional options and no global config)', function (assert) { + assert.equal( + buildBaseURL({ + op: 'findRecord', + identifier: { type: 'user', id: '1' }, + }), + '/user/1', + `buildBaseURL works for findRecord` + ); + assert.equal( + buildBaseURL({ + op: 'updateRecord', + identifier: { type: 'user', id: '1' }, + }), + '/user/1', + `buildBaseURL works for updateRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'deleteRecord', + identifier: { type: 'user', id: '1' }, + }), + '/user/1', + `buildBaseURL works for deleteRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + fieldPath: 'bestFriend', + }), + '/user/1/bestFriend', + `buildBaseURL works for findRelatedRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'findRelatedCollection', + identifier: { type: 'user', id: '1' }, + fieldPath: 'friends', + }), + '/user/1/friends', + `buildBaseURL works for findRelatedCollection` + ); + + assert.equal( + buildBaseURL({ + op: 'query', + identifier: { type: 'user' }, + }), + '/user', + `buildBaseURL works for query` + ); + + assert.equal( + buildBaseURL({ + op: 'findMany', + identifiers: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }), + '/user', + `buildBaseURL works for findMany` + ); + }); + + test('resourcePath (no global config)', function (assert) { + assert.equal( + buildBaseURL({ + op: 'findRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + }), + '/people/1', + `buildBaseURL works for findRecord` + ); + assert.equal( + buildBaseURL({ + op: 'updateRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + }), + '/people/1', + `buildBaseURL works for updateRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'deleteRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + }), + '/people/1', + `buildBaseURL works for deleteRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + }), + '/people/1/bestFriend', + `buildBaseURL works for findRelatedRecord` + ); + + assert.equal( + buildBaseURL({ + op: 'findRelatedCollection', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'friends', + }), + '/people/1/friends', + `buildBaseURL works for findRelatedCollection` + ); + + assert.equal( + buildBaseURL({ + op: 'query', + identifier: { type: 'user' }, + resourcePath: 'people', + }), + '/people', + `buildBaseURL works for query` + ); + + assert.equal( + buildBaseURL({ + op: 'findMany', + identifiers: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + resourcePath: 'people', + }), + '/people', + `buildBaseURL works for findMany` + ); + }); + + test('namespace uses local when present (no global config)', function (assert) { + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + namespace: 'api/v1', + }), + '/api/v1/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('namespace (global config)', function (assert) { + setBuildURLConfig({ namespace: 'api/v2', host: '' }); + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + }), + '/api/v2/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('namespace uses local when present (global config)', function (assert) { + setBuildURLConfig({ namespace: 'api/v2', host: '' }); + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + namespace: 'api/v3', + }), + '/api/v3/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('host uses local when present (no global config)', function (assert) { + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + host: 'https://api.example.com', + }), + 'https://api.example.com/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('host (global config)', function (assert) { + setBuildURLConfig({ namespace: '', host: 'https://api2.example.com' }); + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + }), + 'https://api2.example.com/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('host uses local when present (global config)', function (assert) { + setBuildURLConfig({ namespace: '', host: 'https://api2.example.com' }); + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + fieldPath: 'bestFriend', + host: 'https://api3.example.com', + }), + 'https://api3.example.com/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + test('host may start with a /', function (assert) { + assert.equal( + buildBaseURL({ + op: 'findRelatedRecord', + identifier: { type: 'user', id: '1' }, + resourcePath: 'people', + host: '/api', + fieldPath: 'bestFriend', + }), + '/api/people/1/bestFriend', + `buildBaseURL works as expected` + ); + }); + + debug('throws when no op is provided', async function (assert) { + await assert.expectAssertion(() => { + // @ts-expect-error testing invalid input + buildBaseURL({}); + }, /buildBaseURL: You must pass `op` as part of options/); + }); + + debug('throws when an invalid op is provided', async function (assert) { + await assert.expectAssertion(() => { + // @ts-expect-error testing invalid input + buildBaseURL({ op: 'not-an-op', identifier: { type: 'user', id: '1' } }); + }, /buildBaseURL: You tried to build a not-an-op request to user but op must be one of/); + }); + + debug('throws when no identifier is provided', async function (assert) { + await assert.expectAssertion(() => { + // @ts-expect-error testing invalid input + buildBaseURL({ op: 'findRecord' }); + }, /buildBaseURL: You must pass `identifier` as part of options/); + }); + + debug('throws when identifier is missing type', async function (assert) { + await assert.expectAssertion(() => { + // @ts-expect-error testing invalid input + buildBaseURL({ op: 'findRecord', identifier: { id: '1' } }); + }, /You must pass valid `identifier` as part of options, expected 'type'/); + }); + + debug('throws when identifier is missing id', async function (assert) { + await assert.expectAssertion(() => { + // @ts-expect-error testing invalid input + buildBaseURL({ op: 'findRecord', identifier: { type: 'user' } }); + }, /You must pass valid `identifier` as part of options, expected 'id'/); + }); +}); diff --git a/tests/builders/tests/unit/build-query-params-test.ts b/tests/builders/tests/unit/build-query-params-test.ts new file mode 100644 index 00000000000..12cf7004ee8 --- /dev/null +++ b/tests/builders/tests/unit/build-query-params-test.ts @@ -0,0 +1,138 @@ +import { buildQueryParams } from '@ember-data/request-utils'; +import { module, test } from '@warp-drive/diagnostic'; + +module('buildQueryParams', function (hooks) { + test('It serializes objects with stable key order', function (assert) { + assert.equal( + buildQueryParams({ + foo: 'bar', + baz: 'qux', + }), + 'baz=qux&foo=bar', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams({ + baz: 'qux', + foo: 'bar', + }), + 'baz=qux&foo=bar', + `buildQueryParams works` + ); + }); + + test('It serializes URLSearchParams with stable key order', function (assert) { + const params1 = new URLSearchParams(); + params1.append('foo', 'bar'); + params1.append('baz', 'qux'); + const params2 = new URLSearchParams(); + params2.append('baz', 'qux'); + params2.append('foo', 'bar'); + + assert.equal(buildQueryParams(params1), 'baz=qux&foo=bar', `buildQueryParams works`); + assert.equal(buildQueryParams(params2), 'baz=qux&foo=bar', `buildQueryParams works`); + }); + + test('It serializes objects with stable value order', function (assert) { + assert.equal( + buildQueryParams({ + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }), + 'baz=d%2Ce%2Cf&foo=a%2Cb%2Cc', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams({ + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }), + 'baz=d%2Ce%2Cf&foo=a%2Cb%2Cc', + `buildQueryParams works` + ); + }); + + test('It serializes URLSearchParams with stable value order', function (assert) { + const params1 = new URLSearchParams(); + params1.append('foo', 'c'); + params1.append('foo', 'b'); + params1.append('foo', 'a'); + params1.append('baz', 'f'); + params1.append('baz', 'd'); + params1.append('baz', 'e'); + const params2 = new URLSearchParams(); + params2.append('foo', 'c'); + params2.append('foo', 'b'); + params2.append('foo', 'a'); + params2.append('baz', 'f'); + params2.append('baz', 'd'); + params2.append('baz', 'e'); + + assert.equal(buildQueryParams(params1), 'baz=d%2Ce%2Cf&foo=a%2Cb%2Cc', `buildQueryParams works`); + assert.equal(buildQueryParams(params2), 'baz=d%2Ce%2Cf&foo=a%2Cb%2Cc', `buildQueryParams works`); + }); + + test('It special cases object.include', function (assert) { + assert.equal( + buildQueryParams({ + include: ['foo', 'bar'], + }), + 'include=bar%2Cfoo', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams({ + include: 'foo,bar', + }), + 'include=bar%2Cfoo', + `buildQueryParams works` + ); + }); + + test('It allows for customizing the arrayFormat', function (assert) { + assert.equal( + buildQueryParams( + { + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }, + { arrayFormat: 'bracket' } + ), + 'baz%5B%5D=d&baz%5B%5D=e&baz%5B%5D=f&foo%5B%5D=a&foo%5B%5D=b&foo%5B%5D=c', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams( + { + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }, + { arrayFormat: 'indices' } + ), + 'baz%5B0%5D=d&baz%5B1%5D=e&baz%5B2%5D=f&foo%5B0%5D=a&foo%5B1%5D=b&foo%5B2%5D=c', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams( + { + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }, + { arrayFormat: 'repeat' } + ), + 'baz=d&baz=e&baz=f&foo=a&foo=b&foo=c', + `buildQueryParams works` + ); + assert.equal( + buildQueryParams( + { + foo: ['c', 'b', 'a'], + baz: ['f', 'd', 'e'], + }, + { arrayFormat: 'comma' } + ), + 'baz=d%2Ce%2Cf&foo=a%2Cb%2Cc', + `buildQueryParams works` + ); + }); +}); diff --git a/tests/builders/tests/unit/filter-empty-test.ts b/tests/builders/tests/unit/filter-empty-test.ts new file mode 100644 index 00000000000..fb767a94702 --- /dev/null +++ b/tests/builders/tests/unit/filter-empty-test.ts @@ -0,0 +1,28 @@ +import { filterEmpty } from '@ember-data/request-utils'; +import { module, test } from '@warp-drive/diagnostic'; + +module('filterEmpty', function () { + test('it returns an empty object when given an empty object', function (assert) { + assert.deepEqual(filterEmpty({}), {}); + }); + + test('it returns an object with truthy values and meaningful falsy values like `false` and `0`', function (assert) { + assert.deepEqual( + filterEmpty({ + foo: 'bar', + baz: null, + zero: 0, + booleanFalse: false, + emptyString: '', + emptyArray: [], + fullArray: [1, 2, 3], + }), + { + zero: 0, + booleanFalse: false, + foo: 'bar', + fullArray: [1, 2, 3], + } + ); + }); +}); diff --git a/tests/builders/tests/unit/inflection-deprecation-test.ts b/tests/builders/tests/unit/inflection-deprecation-test.ts new file mode 100644 index 00000000000..4db4c8efbd4 --- /dev/null +++ b/tests/builders/tests/unit/inflection-deprecation-test.ts @@ -0,0 +1,24 @@ +import '@ember-data/request-utils/deprecation-support'; + +import { default as Inflector, singularize as inflectorSingularize } from 'ember-inflector'; + +import { singularize } from '@ember-data/request-utils/string'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('Unit | Inflection Deprecation', function (hooks) { + setupTest(hooks); + + test('Uncountable works as expected', function (assert) { + Inflector.inflector.uncountable('trails'); + + assert.equal(singularize('trails'), 'trails', 'Uncountable rule is applied to @ember-data/request-utils/string'); + assert.equal(inflectorSingularize('trails'), 'trails', 'Uncountable rule is applied to ember-inflector'); + + assert.expectDeprecation({ + id: 'warp-drive.ember-inflector', + count: 1, + until: '6.0.0', + }); + }); +}); diff --git a/tests/builders/tests/unit/inflector-test.ts b/tests/builders/tests/unit/inflector-test.ts new file mode 100644 index 00000000000..236c27cd22d --- /dev/null +++ b/tests/builders/tests/unit/inflector-test.ts @@ -0,0 +1,447 @@ +import { + clear, + clearRules, + irregular, + plural, + pluralize, + resetToDefaults, + singular, + singularize, + uncountable, +} from '@ember-data/request-utils/string'; +import { module, test } from '@warp-drive/diagnostic'; + +module('Inflector', function (hooks) { + hooks.afterEach(() => { + resetToDefaults(); + }); + + module('dsl', () => { + test('ability to add additional pluralization rules', (assert) => { + clearRules(); + assert.equal(pluralize('cow'), 'cow', 'no pluralization rule'); + + plural(/$/, 's'); + clear(); + + assert.equal(pluralize('cow'), 'cows', 'pluralization rule was applied'); + }); + + test('ability to add additional singularization rules', (assert) => { + clearRules(); + assert.equal(singularize('cows'), 'cows', 'no singularization rule was applied'); + + singular(/s$/, ''); + clear(); + + assert.equal(singularize('cows'), 'cow', 'singularization rule was applied'); + }); + + test('ability to add additional uncountable rules', (assert) => { + clearRules(); + plural(/$/, 's'); + assert.equal(pluralize('cow'), 'cows', 'pluralization rule was applied'); + + uncountable('cow'); + clear(); + assert.equal(pluralize('cow'), 'cow', 'pluralization rule NOT was applied'); + assert.equal(pluralize('redCow'), 'redCow', 'pluralization rule NOT was applied'); + assert.equal(pluralize('red-cow'), 'red-cow', 'pluralization rule NOT was applied'); + assert.equal(pluralize('red/cow'), 'red/cow', 'pluralization rule NOT was applied'); + }); + + test('ability to add additional irregular rules', (assert) => { + clearRules(); + singular(/s$/, ''); + plural(/$/, 's'); + + assert.equal(singularize('cows'), 'cow', 'regular singularization rule was applied'); + assert.equal(pluralize('cow'), 'cows', 'regular pluralization rule was applied'); + + assert.equal(singularize('red-cows'), 'red-cow', 'regular singularization rule was applied'); + assert.equal(pluralize('red-cow'), 'red-cows', 'regular pluralization rule was applied'); + + assert.equal(singularize('redCows'), 'redCow', 'regular singularization rule was applied'); + assert.equal(pluralize('redCow'), 'redCows', 'regular pluralization rule was applied'); + + assert.equal(singularize('red/cows'), 'red/cow', 'regular singularization rule was applied'); + assert.equal(pluralize('red/cow'), 'red/cows', 'regular pluralization rule was applied'); + + irregular('cow', 'kine'); + clear(); + + assert.equal(singularize('kine'), 'cow', 'irregular singularization rule was applied'); + assert.equal(pluralize('cow'), 'kine', 'irregular pluralization rule was applied'); + + assert.equal(singularize('red-kine'), 'red-cow', 'irregular singularization rule was applied'); + assert.equal(pluralize('red-cow'), 'red-kine', 'irregular pluralization rule was applied'); + + assert.equal( + singularize('red-red-cow'), + 'red-red-cow', + 'irregular singularization rule was applied correctly with dasherization' + ); + assert.equal( + singularize('red-red-kine'), + 'red-red-cow', + 'irregular singularization rule was applied correctly with dasherization' + ); + assert.equal( + pluralize('red-red-cow'), + 'red-red-kine', + 'irregular pluralization rule was applied correctly with dasherization' + ); + assert.equal( + pluralize('red-red-kine'), + 'red-red-kine', + 'irregular pluralization rule was applied correctly with dasherization' + ); + + assert.equal(singularize('redKine'), 'redCow', 'irregular singularization rule was applied'); + assert.equal(pluralize('redCow'), 'redKine', 'irregular pluralization rule was applied'); + + assert.equal(singularize('red/kine'), 'red/cow', 'irregular singularization rule was applied'); + assert.equal(pluralize('red/cow'), 'red/kine', 'irregular pluralization rule was applied'); + }); + + test('ability to add identical singular and pluralizations', (assert) => { + clearRules(); + singular(/s$/, ''); + plural(/$/, 's'); + + assert.equal(singularize('settings'), 'setting', 'regular singularization rule was applied'); + assert.equal(pluralize('setting'), 'settings', 'regular pluralization rule was applied'); + + irregular('settings', 'settings'); + irregular('userPreferences', 'userPreferences'); + clear(); + + assert.equal(singularize('settings'), 'settings', 'irregular singularization rule was applied on lowercase word'); + assert.equal(pluralize('settings'), 'settings', 'irregular pluralization rule was applied on lowercase word'); + + assert.equal( + singularize('userPreferences'), + 'userPreferences', + 'irregular singularization rule was applied on camelcase word' + ); + assert.equal( + pluralize('userPreferences'), + 'userPreferences', + 'irregular pluralization rule was applied on camelcase word' + ); + }); + }); + + module('unit', () => { + test('plurals', (assert) => { + const plurals: Array<[RegExp, string]> = [ + [/$/, 's'], + [/s$/i, 's'], + ]; + plurals.forEach((v) => plural(v[0], v[1])); + + assert.equal(pluralize('apple'), 'apples'); + }); + + test('singularization', (assert) => { + const singulars: Array<[RegExp, string]> = [ + [/s$/i, ''], + [/(ss)$/i, '$1'], + ]; + singulars.forEach((v) => singular(v[0], v[1])); + + assert.equal(singularize('apple'), 'apple'); + }); + + test('singularization of irregular singulars', (assert) => { + const singulars: Array<[RegExp, string]> = [ + [/s$/i, ''], + [/(ss)$/i, '$1'], + ]; + singulars.forEach((v) => singular(v[0], v[1])); + irregular('lens', 'lenses'); + + assert.equal(singularize('lens'), 'lens'); + }); + + test('pluralization of irregular plurals', (assert) => { + assert.equal(pluralize('people'), 'people'); + }); + + test('irregular', (assert) => { + irregular('1', '12'); + irregular('2', '22'); + irregular('3', '32'); + irregular('word', 'wordy'); + + assert.equal(pluralize('1'), '12'); + assert.equal(pluralize('2'), '22'); + assert.equal(pluralize('3'), '32'); + assert.equal(pluralize('word'), 'wordy'); + + assert.equal(singularize('12'), '1'); + assert.equal(singularize('22'), '2'); + assert.equal(singularize('32'), '32'); // because the rule for '2' takes precedence :facepalm: + assert.equal(singularize('wordy'), 'word'); + }); + + test('uncountable', (assert) => { + uncountable('1'); + uncountable('2'); + uncountable('3'); + + assert.equal(pluralize('1'), '1'); + assert.equal(pluralize('2'), '2'); + assert.equal(pluralize('3'), '3'); + assert.equal(singularize('1'), '1'); + assert.equal(singularize('2'), '2'); + assert.equal(singularize('3'), '3'); + }); + + test('defaultRules matches docs', (assert) => { + // defaultRules includes these special rules + assert.equal(pluralize('cow'), 'kine'); + assert.equal(singularize('kine'), 'cow'); + + // defaultRules adds 's' to singular + assert.equal(pluralize('item'), 'items'); + + // defaultRules removes 's' from plural + assert.equal(singularize('items'), 'item'); + }); + + test('words containing irregular and uncountable words can be pluralized', (assert) => { + assert.equal(pluralize('woman'), 'women'); + assert.equal(pluralize('salesperson'), 'salespeople'); + }); + + test('words containing irregular and uncountable words can be singularized', (assert) => { + assert.equal(singularize('women'), 'woman'); + assert.equal(singularize('salespeople'), 'salesperson'); + assert.equal(singularize('pufferfish'), 'pufferfish'); + }); + + test('partial words containing uncountable words can be pluralized', (assert) => { + assert.equal(pluralize('price'), 'prices'); + }); + + test('partial words containing uncountable words can be singularized', (assert) => { + assert.equal(singularize('subspecies'), 'subspecy'); + }); + + test('CamelCase and UpperCamelCase is preserved for irregular and uncountable pluralizations', (assert) => { + assert.equal(pluralize('SuperWoman'), 'SuperWomen'); + assert.equal(pluralize('superWoman'), 'superWomen'); + assert.equal(pluralize('SuperMan'), 'SuperMen'); + assert.equal(pluralize('superMan'), 'superMen'); + assert.equal(pluralize('FriedRice'), 'FriedRice'); + assert.equal(pluralize('friedRice'), 'friedRice'); + }); + + test('CamelCase and UpperCamelCase is preserved for irregular and uncountable singularization', (assert) => { + assert.equal(singularize('SuperWomen'), 'SuperWoman'); + assert.equal(singularize('superWomen'), 'superWoman'); + assert.equal(singularize('SuperMen'), 'SuperMan'); + assert.equal(singularize('superMen'), 'superMan'); + assert.equal(singularize('FriedRice'), 'FriedRice'); + assert.equal(singularize('friedRice'), 'friedRice'); + }); + + test('CamelCase custom irregular words', (assert) => { + irregular('unitOfMeasure', 'unitsOfMeasure'); + irregular('tipoDocumento', 'tiposDocumento'); + + assert.equal(singularize('unitsOfMeasure'), 'unitOfMeasure'); + assert.equal(pluralize('unitOfMeasure'), 'unitsOfMeasure'); + + assert.equal(singularize('tiposDocumento'), 'tipoDocumento'); + assert.equal(pluralize('tipoDocumento'), 'tiposDocumento'); + }); + + test('pluralize passes same test cases as ActiveSupport::Inflector#pluralize', (assert) => { + assert.equal(pluralize('search'), 'searches'); + assert.equal(pluralize('switch'), 'switches'); + assert.equal(pluralize('fix'), 'fixes'); + assert.equal(pluralize('box'), 'boxes'); + assert.equal(pluralize('process'), 'processes'); + assert.equal(pluralize('address'), 'addresses'); + assert.equal(pluralize('case'), 'cases'); + assert.equal(pluralize('stack'), 'stacks'); + assert.equal(pluralize('wish'), 'wishes'); + assert.equal(pluralize('fish'), 'fish'); + assert.equal(pluralize('jeans'), 'jeans'); + assert.equal(pluralize('funky jeans'), 'funky jeans'); + assert.equal(pluralize('my money'), 'my money'); + assert.equal(pluralize('category'), 'categories'); + assert.equal(pluralize('query'), 'queries'); + assert.equal(pluralize('ability'), 'abilities'); + assert.equal(pluralize('agency'), 'agencies'); + assert.equal(pluralize('movie'), 'movies'); + assert.equal(pluralize('archive'), 'archives'); + assert.equal(pluralize('index'), 'indices'); + assert.equal(pluralize('wife'), 'wives'); + assert.equal(pluralize('safe'), 'saves'); + assert.equal(pluralize('half'), 'halves'); + assert.equal(pluralize('move'), 'moves'); + assert.equal(pluralize('salesperson'), 'salespeople'); + assert.equal(pluralize('person'), 'people'); + assert.equal(pluralize('spokesman'), 'spokesmen'); + assert.equal(pluralize('man'), 'men'); + assert.equal(pluralize('woman'), 'women'); + assert.equal(pluralize('basis'), 'bases'); + assert.equal(pluralize('diagnosis'), 'diagnoses'); + assert.equal(pluralize('diagnosis_a'), 'diagnosis_as'); + assert.equal(pluralize('datum'), 'data'); + assert.equal(pluralize('medium'), 'media'); + assert.equal(pluralize('stadium'), 'stadia'); + assert.equal(pluralize('analysis'), 'analyses'); + assert.equal(pluralize('my_analysis'), 'my_analyses'); + assert.equal(pluralize('node_child'), 'node_children'); + assert.equal(pluralize('child'), 'children'); + assert.equal(pluralize('experience'), 'experiences'); + assert.equal(pluralize('day'), 'days'); + assert.equal(pluralize('comment'), 'comments'); + assert.equal(pluralize('foobar'), 'foobars'); + assert.equal(pluralize('newsletter'), 'newsletters'); + assert.equal(pluralize('old_news'), 'old_news'); + assert.equal(pluralize('news'), 'news'); + assert.equal(pluralize('series'), 'series'); + assert.equal(pluralize('miniseries'), 'miniseries'); + assert.equal(pluralize('species'), 'species'); + assert.equal(pluralize('quiz'), 'quizzes'); + assert.equal(pluralize('perspective'), 'perspectives'); + assert.equal(pluralize('ox'), 'oxen'); + assert.equal(pluralize('photo'), 'photos'); + assert.equal(pluralize('buffalo'), 'buffaloes'); + assert.equal(pluralize('tomato'), 'tomatoes'); + assert.equal(pluralize('dwarf'), 'dwarves'); + assert.equal(pluralize('elf'), 'elves'); + assert.equal(pluralize('information'), 'information'); + assert.equal(pluralize('equipment'), 'equipment'); + assert.equal(pluralize('bus'), 'buses'); + assert.equal(pluralize('status'), 'statuses'); + assert.equal(pluralize('status_code'), 'status_codes'); + assert.equal(pluralize('mouse'), 'mice'); + assert.equal(pluralize('louse'), 'lice'); + assert.equal(pluralize('house'), 'houses'); + assert.equal(pluralize('octopus'), 'octopi'); + assert.equal(pluralize('virus'), 'viri'); + assert.equal(pluralize('alias'), 'aliases'); + assert.equal(pluralize('portfolio'), 'portfolios'); + assert.equal(pluralize('vertex'), 'vertices'); + assert.equal(pluralize('matrix'), 'matrices'); + assert.equal(pluralize('matrix_fu'), 'matrix_fus'); + assert.equal(pluralize('axis'), 'axes'); + assert.equal(pluralize('taxi'), 'taxis'); + assert.equal(pluralize('testis'), 'testes'); + assert.equal(pluralize('crisis'), 'crises'); + assert.equal(pluralize('rice'), 'rice'); + assert.equal(pluralize('shoe'), 'shoes'); + assert.equal(pluralize('horse'), 'horses'); + assert.equal(pluralize('prize'), 'prizes'); + assert.equal(pluralize('edge'), 'edges'); + assert.equal(pluralize('database'), 'databases'); + assert.equal(pluralize('|ice'), '|ices'); + assert.equal(pluralize('|ouse'), '|ouses'); + assert.equal(pluralize('slice'), 'slices'); + assert.equal(pluralize('police'), 'police'); + }); + + test('singularize passes same test cases as ActiveSupport::Inflector#singularize', (assert) => { + assert.equal(singularize('searches'), 'search'); + assert.equal(singularize('switches'), 'switch'); + assert.equal(singularize('fixes'), 'fix'); + assert.equal(singularize('boxes'), 'box'); + assert.equal(singularize('processes'), 'process'); + assert.equal(singularize('addresses'), 'address'); + assert.equal(singularize('cases'), 'case'); + assert.equal(singularize('stacks'), 'stack'); + assert.equal(singularize('wishes'), 'wish'); + assert.equal(singularize('fish'), 'fish'); + assert.equal(singularize('jeans'), 'jeans'); + assert.equal(singularize('funky jeans'), 'funky jeans'); + assert.equal(singularize('my money'), 'my money'); + assert.equal(singularize('categories'), 'category'); + assert.equal(singularize('queries'), 'query'); + assert.equal(singularize('abilities'), 'ability'); + assert.equal(singularize('agencies'), 'agency'); + assert.equal(singularize('movies'), 'movie'); + assert.equal(singularize('archives'), 'archive'); + assert.equal(singularize('indices'), 'index'); + assert.equal(singularize('wives'), 'wife'); + assert.equal(singularize('saves'), 'safe'); + assert.equal(singularize('halves'), 'half'); + assert.equal(singularize('moves'), 'move'); + assert.equal(singularize('salespeople'), 'salesperson'); + assert.equal(singularize('people'), 'person'); + assert.equal(singularize('spokesmen'), 'spokesman'); + assert.equal(singularize('men'), 'man'); + assert.equal(singularize('women'), 'woman'); + assert.equal(singularize('bases'), 'basis'); + assert.equal(singularize('diagnoses'), 'diagnosis'); + assert.equal(singularize('diagnosis_as'), 'diagnosis_a'); + assert.equal(singularize('data'), 'datum'); + assert.equal(singularize('media'), 'medium'); + assert.equal(singularize('stadia'), 'stadium'); + assert.equal(singularize('analyses'), 'analysis'); + assert.equal(singularize('my_analyses'), 'my_analysis'); + assert.equal(singularize('node_children'), 'node_child'); + assert.equal(singularize('children'), 'child'); + assert.equal(singularize('experiences'), 'experience'); + assert.equal(singularize('days'), 'day'); + assert.equal(singularize('comments'), 'comment'); + assert.equal(singularize('foobars'), 'foobar'); + assert.equal(singularize('newsletters'), 'newsletter'); + assert.equal(singularize('old_news'), 'old_news'); + assert.equal(singularize('news'), 'news'); + assert.equal(singularize('series'), 'series'); + assert.equal(singularize('miniseries'), 'miniseries'); + assert.equal(singularize('species'), 'species'); + assert.equal(singularize('quizzes'), 'quiz'); + assert.equal(singularize('perspectives'), 'perspective'); + assert.equal(singularize('oxen'), 'ox'); + assert.equal(singularize('photos'), 'photo'); + assert.equal(singularize('buffaloes'), 'buffalo'); + assert.equal(singularize('tomatoes'), 'tomato'); + assert.equal(singularize('dwarves'), 'dwarf'); + assert.equal(singularize('elves'), 'elf'); + assert.equal(singularize('information'), 'information'); + assert.equal(singularize('equipment'), 'equipment'); + assert.equal(singularize('buses'), 'bus'); + assert.equal(singularize('statuses'), 'status'); + assert.equal(singularize('status_codes'), 'status_code'); + assert.equal(singularize('mice'), 'mouse'); + assert.equal(singularize('lice'), 'louse'); + assert.equal(singularize('houses'), 'house'); + assert.equal(singularize('octopi'), 'octopus'); + assert.equal(singularize('viri'), 'virus'); + assert.equal(singularize('aliases'), 'alias'); + assert.equal(singularize('portfolios'), 'portfolio'); + assert.equal(singularize('vertices'), 'vertex'); + assert.equal(singularize('matrices'), 'matrix'); + assert.equal(singularize('matrix_fus'), 'matrix_fu'); + assert.equal(singularize('axes'), 'axis'); + assert.equal(singularize('taxis'), 'taxi'); + assert.equal(singularize('testes'), 'testis'); + assert.equal(singularize('crises'), 'crisis'); + assert.equal(singularize('rice'), 'rice'); + assert.equal(singularize('shoes'), 'shoe'); + assert.equal(singularize('horses'), 'horse'); + assert.equal(singularize('prizes'), 'prize'); + assert.equal(singularize('edges'), 'edge'); + assert.equal(singularize('databases'), 'database'); + assert.equal(singularize('|ices'), '|ice'); + assert.equal(singularize('|ouses'), '|ouse'); + assert.equal(singularize('slices'), 'slice'); + assert.equal(singularize('police'), 'police'); + }); + + test('singularize can singularize "bonuses"', (assert) => { + assert.equal(singularize('bonuses'), 'bonus'); + }); + + test('singularize can pluralize "bonus"', (assert) => { + assert.equal(pluralize('bonus'), 'bonuses'); + }); + }); +}); diff --git a/tests/builders/tests/unit/json-api-builder-test.ts b/tests/builders/tests/unit/json-api-builder-test.ts new file mode 100644 index 00000000000..2296d371a98 --- /dev/null +++ b/tests/builders/tests/unit/json-api-builder-test.ts @@ -0,0 +1,322 @@ +import type { TestContext } from '@ember/test-helpers'; + +import { createRecord, deleteRecord, findRecord, postQuery, query, updateRecord } from '@ember-data/json-api/request'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +import type UserSetting from '../../app/models/user-setting'; +import { headersToObject } from '../helpers/utils'; + +const JSON_API_HEADERS = { accept: 'application/vnd.api+json' }; + +module('JSON:API | Request Builders', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('findRecord by identifier', function (this: TestContext, assert) { + const result = findRecord({ type: 'user-setting', id: '1' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/1', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('findRecord by type+id', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1'); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/1', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('findRecord by identifier with options', function (this: TestContext, assert) { + const result = findRecord( + { type: 'user-setting', id: '1' }, + { reload: true, backgroundReload: false, include: 'user,friends' } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { + reload: true, + backgroundReload: false, + }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier and options` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('findRecord by type+id with options', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1', { reload: true, backgroundReload: false, include: 'user,friends' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id and options` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('query', function (this: TestContext, assert) { + const result = query( + 'user-setting', + { include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }, + { reload: true, backgroundReload: false } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and options` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('query with empty params [used to be findAll]', function (this: TestContext, assert) { + const result = query('user-setting', {}, { reload: true, backgroundReload: false }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings', + method: 'GET', + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and empty options, does not leave a trailing ?` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('postQuery', function (this: TestContext, assert) { + const result = postQuery( + 'user-setting', + { include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }, + { reload: true, backgroundReload: false } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings', + method: 'POST', + body: JSON.stringify({ include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }), + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { + reload: true, + backgroundReload: false, + key: 'https://api.example.com/api/v1/user-settings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc', + }, + op: 'query', + }, + `query works with type and options` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + + test('createRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings', + method: 'POST', + headers: new Headers(JSON_API_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS, "headers are set to JSON API's"); + }); + + test('createRecord passing store record and options', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting, { resourcePath: 'user-settings/new' }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/new', + method: 'POST', + headers: new Headers(JSON_API_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS, "headers are set to JSON API's"); + }); + + test('updateRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/12', + method: 'PUT', + headers: new Headers(JSON_API_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS, "headers are set to JSON API's"); + }); + + test('updateRecord with PATCH method', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting, { patch: true }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/12', + method: 'PATCH', + headers: new Headers(JSON_API_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS, "headers are set to JSON API's"); + }); + + test('deleteRecord with identifier', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12'); + const identifier = recordIdentifierFor(userSetting); + + const result = deleteRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings/12', + method: 'DELETE', + headers: new Headers(JSON_API_HEADERS), + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `deleteRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS, "headers are set to JSON API's"); + }); +}); diff --git a/tests/builders/tests/unit/parse-cache-control-test.ts b/tests/builders/tests/unit/parse-cache-control-test.ts new file mode 100644 index 00000000000..43e06cbe796 --- /dev/null +++ b/tests/builders/tests/unit/parse-cache-control-test.ts @@ -0,0 +1,68 @@ +import { parseCacheControl } from '@ember-data/request-utils'; +import { test as debug } from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { module, test } from '@warp-drive/diagnostic'; + +module('parseCacheControl', function (hooks) { + test('should parse a single Cache-Control directive', function (assert) { + const header = 'max-age=3600'; + const result = parseCacheControl(header); + assert.deepEqual(result, { 'max-age': 3600 }); + }); + + test('should parse multiple Cache-Control directives', function (assert) { + const header = 'max-age=3600, must-revalidate'; + const result = parseCacheControl(header); + assert.deepEqual(result, { 'max-age': 3600, 'must-revalidate': true }); + }); + + test('should parse Cache-Control directives with multiple delta-seconds values', function (assert) { + const header = 'max-age=3600, s-maxage=7200'; + const result = parseCacheControl(header); + assert.deepEqual(result, { 'max-age': 3600, 's-maxage': 7200 }); + }); + + test('should parse Cache-Control directives with a single token value', function (assert) { + const header = 'no-cache'; + const result = parseCacheControl(header); + assert.deepEqual(result, { 'no-cache': true }); + }); + + test('should parse Cache-Control directives with multiple token values', function (assert) { + const header = 'no-cache, no-store'; + const result = parseCacheControl(header); + assert.deepEqual(result, { 'no-cache': true, 'no-store': true }); + }); + + test('should parse Cache-Control directives with a single byte-range-set value', function (assert) { + const header = + 'max-age=3600, no-transform, only-if-cached, public, must-revalidate, proxy-revalidate, no-cache, s-maxage=7200, stale-while-revalidate=3600, stale-if-error=7200, immutable'; + const result = parseCacheControl(header); + assert.deepEqual(result, { + 'max-age': 3600, + 'no-transform': true, + 'only-if-cached': true, + public: true, + 'must-revalidate': true, + 'proxy-revalidate': true, + 'no-cache': true, + 's-maxage': 7200, + 'stale-while-revalidate': 3600, + 'stale-if-error': 7200, + immutable: true, + }); + }); + + debug('throws when Cache-Control has invalid directives', async function (assert) { + await assert.expectAssertion(() => { + const header = 'max-age=,'; + parseCacheControl(header); + }, /Invalid Cache-Control value, expected a value after "=" but got ","/); + }); + + debug('throws when Cache-Control has invalid value type', async function (assert) { + await assert.expectAssertion(() => { + const header = 'max-age="3600"'; + parseCacheControl(header); + }, /Invalid Cache-Control value, expected a number but got - "3600"/); + }); +}); diff --git a/tests/builders/tests/unit/rest-builder-test.ts b/tests/builders/tests/unit/rest-builder-test.ts new file mode 100644 index 00000000000..224cbeeb8b1 --- /dev/null +++ b/tests/builders/tests/unit/rest-builder-test.ts @@ -0,0 +1,297 @@ +import type { TestContext } from '@ember/test-helpers'; + +import { setBuildURLConfig } from '@ember-data/request-utils'; +import { createRecord, deleteRecord, findRecord, query, updateRecord } from '@ember-data/rest/request'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +import type UserSetting from '../../app/models/user-setting'; +import { headersToObject } from '../helpers/utils'; + +const REST_HEADERS = { accept: 'application/json;charset=utf-8' }; + +module('REST | Request Builders', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + setBuildURLConfig({ host: 'https://api.example.com', namespace: 'api/v1' }); + }); + + hooks.afterEach(function () { + setBuildURLConfig({ host: '', namespace: '' }); + }); + + test('findRecord by identifier', function (this: TestContext, assert) { + const result = findRecord({ type: 'user-setting', id: '1' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/1', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('findRecord by type+id', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1'); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/1', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: {}, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('findRecord by identifier with options', function (this: TestContext, assert) { + const result = findRecord( + { type: 'user-setting', id: '1' }, + { reload: true, backgroundReload: false, include: 'user,friends' } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: { + reload: true, + backgroundReload: false, + }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with an identifier and options` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('findRecord by type+id with options', function (this: TestContext, assert) { + const result = findRecord('user-setting', '1', { reload: true, backgroundReload: false, include: 'user,friends' }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/1?include=friends%2Cuser', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'findRecord', + records: [{ type: 'user-setting', id: '1' }], + }, + `findRecord works with type+id and options` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('query', function (this: TestContext, assert) { + const result = query( + 'user-setting', + { include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }, + { reload: true, backgroundReload: false } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and options` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('query with empty params [used to be findAll]', function (this: TestContext, assert) { + const result = query('user-setting', {}, { reload: true, backgroundReload: false }); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings', + method: 'GET', + headers: new Headers(REST_HEADERS), + cacheOptions: { reload: true, backgroundReload: false }, + op: 'query', + }, + `query works with type and empty options, does not leave a trailing ?` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS); + }); + + test('createRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings', + method: 'POST', + headers: new Headers(REST_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS, "headers are set to REST API's"); + }); + + test('createRecord passing store record and options', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + const userSetting = store.createRecord('user-setting', { + name: 'test', + }); + const identifier = recordIdentifierFor(userSetting); + const result = createRecord(userSetting, { resourcePath: 'userSettings/new' }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/new', + method: 'POST', + headers: new Headers(REST_HEADERS), + op: 'createRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `createRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS, "headers are set to REST API's"); + }); + + test('updateRecord passing store record', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/12', + method: 'PUT', + headers: new Headers(REST_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with record identifier passed` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS, "headers are set to REST API's"); + }); + + test('updateRecord with PATCH method', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12') as UserSetting; + const identifier = recordIdentifierFor(userSetting); + + userSetting.name = 'test2'; + + const result = updateRecord(userSetting, { patch: true }); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/12', + method: 'PATCH', + headers: new Headers(REST_HEADERS), + op: 'updateRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `updateRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS, "headers are set to REST API's"); + }); + + test('deleteRecord with identifier', function (this: TestContext, assert) { + const store = this.owner.lookup('service:store') as Store; + + const expectedData = { + data: { + id: '12', + type: 'user-setting', + attributes: { + name: 'test', + }, + }, + }; + store.push(expectedData); + + const userSetting = store.peekRecord('user-setting', '12'); + const identifier = recordIdentifierFor(userSetting); + + const result = deleteRecord(userSetting); + + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/userSettings/12', + method: 'DELETE', + headers: new Headers(REST_HEADERS), + op: 'deleteRecord', + data: { + record: identifier, + }, + records: [identifier], + }, + `deleteRecord works with patch option` + ); + assert.deepEqual(headersToObject(result.headers), REST_HEADERS, "headers are set to REST API's"); + }); +}); diff --git a/tests/builders/tests/unit/string-utils-test.ts b/tests/builders/tests/unit/string-utils-test.ts new file mode 100644 index 00000000000..ca6b5c204d6 --- /dev/null +++ b/tests/builders/tests/unit/string-utils-test.ts @@ -0,0 +1,64 @@ +import { camelize, capitalize, dasherize, underscore } from '@ember-data/request-utils/string'; +import { module, test } from '@warp-drive/diagnostic'; +import type { Diagnostic } from '@warp-drive/diagnostic/-types'; + +const createTestFunction = (assert: Diagnostic, fn: (v: string) => string) => { + return (given: string, expected: string, description: string) => { + assert.equal(fn(given), expected, description); + }; +}; + +module('String Utils', function () { + test('camelize tests', function (assert) { + const expect = createTestFunction(assert, camelize); + + expect('my favorite items', 'myFavoriteItems', 'camelize normal string'); + expect('I Love Ramen', 'iLoveRamen', 'camelize capitalized string'); + expect('css-class-name', 'cssClassName', 'camelize dasherized string'); + expect('action_name', 'actionName', 'camelize underscored string'); + expect('action.name', 'actionName', 'camelize dot notation string'); + expect('innerHTML', 'innerHTML', 'does nothing with camelcased string'); + expect('PrivateDocs/OwnerInvoice', 'privateDocs/ownerInvoice', 'camelize namespaced classified string'); + expect('private_docs/owner_invoice', 'privateDocs/ownerInvoice', 'camelize namespaced underscored string'); + expect('private-docs/owner-invoice', 'privateDocs/ownerInvoice', 'camelize namespaced dasherized string'); + }); + + test('capitalize tests', function (assert) { + const expect = createTestFunction(assert, capitalize); + + expect('my favorite items', 'My favorite items', 'capitalize normal string'); + expect('css-class-name', 'Css-class-name', 'capitalize dasherized string'); + expect('action_name', 'Action_name', 'capitalize underscored string'); + expect('innerHTML', 'InnerHTML', 'capitalize camelcased string'); + expect('Capitalized string', 'Capitalized string', 'does nothing with capitalized string'); + expect('privateDocs/ownerInvoice', 'PrivateDocs/OwnerInvoice', 'capitalize namespaced camelized string'); + expect('private_docs/owner_invoice', 'Private_docs/Owner_invoice', 'capitalize namespaced underscored string'); + expect('private-docs/owner-invoice', 'Private-docs/Owner-invoice', 'capitalize namespaced dasherized string'); + expect('šabc', 'Šabc', 'capitalize string with accent character'); + }); + + test('dasherize tests', function (assert) { + const expect = createTestFunction(assert, dasherize); + + expect('my favorite items', 'my-favorite-items', 'dasherize normal string'); + expect('css-class-name', 'css-class-name', 'does nothing with dasherized string'); + expect('action_name', 'action-name', 'dasherize underscored string'); + expect('innerHTML', 'inner-html', 'dasherize camelcased string'); + expect('toString', 'to-string', 'dasherize string that is the property name of Object.prototype'); + expect('PrivateDocs/OwnerInvoice', 'private-docs/owner-invoice', 'dasherize namespaced classified string'); + expect('privateDocs/ownerInvoice', 'private-docs/owner-invoice', 'dasherize namespaced camelized string'); + expect('private_docs/owner_invoice', 'private-docs/owner-invoice', 'dasherize namespaced underscored string'); + }); + + test('underscore tests', function (assert) { + const expect = createTestFunction(assert, underscore); + + expect('my favorite items', 'my_favorite_items', 'with normal string'); + expect('css-class-name', 'css_class_name', 'with dasherized string'); + expect('action_name', 'action_name', 'does nothing with underscored string'); + expect('innerHTML', 'inner_html', 'with camelcased string'); + expect('PrivateDocs/OwnerInvoice', 'private_docs/owner_invoice', 'underscore namespaced classified string'); + expect('privateDocs/ownerInvoice', 'private_docs/owner_invoice', 'underscore namespaced camelized string'); + expect('private-docs/owner-invoice', 'private_docs/owner_invoice', 'underscore namespaced dasherized string'); + }); +}); diff --git a/tests/builders/tsconfig.json b/tests/builders/tsconfig.json new file mode 100644 index 00000000000..e0e1d8e71a7 --- /dev/null +++ b/tests/builders/tsconfig.json @@ -0,0 +1,104 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*", "../../@types/ember-data-qunit-asserts"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/active-record": ["../../packages/active-record/unstable-preview-types"], + "@ember-data/active-record/*": ["../../packages/active-record/unstable-preview-types/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/rest": ["../../packages/rest/unstable-preview-types"], + "@ember-data/rest/*": ["../../packages/rest/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@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/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"], + "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/active-record" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/rest" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/diagnostic" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/debug-encapsulation/.ember-cli b/tests/debug-encapsulation/.ember-cli deleted file mode 100644 index ee64cfed2a8..00000000000 --- a/tests/debug-encapsulation/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/tests/debug-encapsulation/.gitignore b/tests/debug-encapsulation/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/debug-encapsulation/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/debug-encapsulation/.template-lintrc.js b/tests/debug-encapsulation/.template-lintrc.js deleted file mode 100644 index f35f61c7b3a..00000000000 --- a/tests/debug-encapsulation/.template-lintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - extends: 'recommended', -}; diff --git a/tests/debug-encapsulation/.watchmanconfig b/tests/debug-encapsulation/.watchmanconfig deleted file mode 100644 index e7834e3e4f3..00000000000 --- a/tests/debug-encapsulation/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/tests/debug-encapsulation/README.md b/tests/debug-encapsulation/README.md deleted file mode 100644 index 450c917cc8a..00000000000 --- a/tests/debug-encapsulation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# encapsulation-test-app - -This README outlines the details of collaborating on this Ember application. -A short introduction of this app could easily go here. - -## Prerequisites - -You will need the following things properly installed on your computer. - -* [Git](https://git-scm.com/) -* [Node.js](https://nodejs.org/) (with npm) -* [Ember CLI](https://ember-cli.com/) -* [Google Chrome](https://google.com/chrome/) - -## Installation - -* `git clone ` this repository -* `cd encapsulation-test-app` -* `npm install` - -## Running / Development - -* `ember serve` -* Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). - -### Code Generators - -Make use of the many generators for code, try `ember help generate` for more details - -### Running Tests - -* `ember test` -* `ember test --server` - -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - -### Building - -* `ember build` (development) -* `ember build --environment production` (production) - -### Deploying - -Specify what it takes to deploy your app. - -## Further Reading / Useful Links - -* [ember.js](https://emberjs.com/) -* [ember-cli](https://ember-cli.com/) -* Development Browser Extensions - * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) - * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/debug-encapsulation/app/index.html b/tests/debug-encapsulation/app/index.html deleted file mode 100644 index 09d6eeaafd2..00000000000 --- a/tests/debug-encapsulation/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EncapsulationTestApp - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/debug-encapsulation/app/templates/application.hbs b/tests/debug-encapsulation/app/templates/application.hbs deleted file mode 100644 index ebe6a496046..00000000000 --- a/tests/debug-encapsulation/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
- {{outlet}} -
\ No newline at end of file diff --git a/tests/debug-encapsulation/config/environment.js b/tests/debug-encapsulation/config/environment.js deleted file mode 100644 index c3e2289a507..00000000000 --- a/tests/debug-encapsulation/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'debug-encapsulation-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/debug-encapsulation/ember-cli-build.js b/tests/debug-encapsulation/ember-cli-build.js deleted file mode 100644 index 8f86cbd8ec6..00000000000 --- a/tests/debug-encapsulation/ember-cli-build.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/tests/debug-encapsulation/package.json b/tests/debug-encapsulation/package.json deleted file mode 100644 index 018a65c217a..00000000000 --- a/tests/debug-encapsulation/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "debug-encapsulation-test-app", - "version": "4.12.8", - "private": true, - "description": "Small description for encapsulation-test-app goes here", - "repository": { - "type": "git", - "url": "https://github.com/emberjs/data.git", - "directory": "tests/debug-encapsulation" - }, - "license": "MIT", - "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", - "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "webpack": "^5.77.0" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "ember": { - "edition": "octane" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/debug-encapsulation/testem.js b/tests/debug-encapsulation/testem.js deleted file mode 100644 index e10b064501a..00000000000 --- a/tests/debug-encapsulation/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - }, - Firefox: { - ci: ['-headless', '-width 1440', '-height 900'], - }, -}; diff --git a/tests/debug-encapsulation/tests/index.html b/tests/debug-encapsulation/tests/index.html deleted file mode 100644 index 3a7bbd20415..00000000000 --- a/tests/debug-encapsulation/tests/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - EncapsulationTestApp Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - diff --git a/tests/debug-encapsulation/tests/integration/smoke-test.js b/tests/debug-encapsulation/tests/integration/smoke-test.js deleted file mode 100644 index a92c15dc5fd..00000000000 --- a/tests/debug-encapsulation/tests/integration/smoke-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* global require */ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -function assertPackageNotPresent(packageName, assert) { - const entries = Object.keys(require.entries); - const entriesFromPackage = entries.filter((m) => m.indexOf(packageName) === 0); - const importedDependencies = {}; - const entriesImportingPackage = entries.filter((m) => { - const deps = require.entries[m].deps; - const moduleDeps = deps.filter((d) => d.indexOf(packageName) === 0); - - if (moduleDeps.length) { - importedDependencies[m] = moduleDeps; - } - return moduleDeps.length > 0; - }); - - assert.ok(entries.length > 0, 'We have modules'); - assert.ok( - entriesFromPackage.length === 0, - `We expect no modules from ${packageName} ${ - entriesFromPackage.length > 0 ? `found: [\n\t"${entriesFromPackage.join('",\n\t"')}"\n]` : '' - }` - ); - assert.ok( - entriesImportingPackage.length === 0, - `We expect no modules with dependencies on ${packageName} ${ - entriesImportingPackage.length > 0 ? `found:\n${JSON.stringify(importedDependencies, null, 2)}` : '' - }` - ); -} - -module('Debug Encapsulation - Smoke Tests', function (hooks) { - setupTest(hooks); - - test('No @ember-data/debug modules are present', function (assert) { - assertPackageNotPresent('@ember-data/debug', assert); - }); - - test('No ember-data modules are present', function (assert) { - assertPackageNotPresent('ember-data', assert); - }); -}); diff --git a/tests/debug-encapsulation/tests/test-helper.js b/tests/debug-encapsulation/tests/test-helper.js deleted file mode 100644 index a16f69329b5..00000000000 --- a/tests/debug-encapsulation/tests/test-helper.js +++ /dev/null @@ -1,23 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/tests/debug-encapsulation/tests/unit/.gitkeep b/tests/debug-encapsulation/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/debug-encapsulation/vendor/.gitkeep b/tests/debug-encapsulation/vendor/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/docs/eslint.config.mjs b/tests/docs/eslint.config.mjs new file mode 100644 index 00000000000..060710f4537 --- /dev/null +++ b/tests/docs/eslint.config.mjs @@ -0,0 +1,23 @@ +// @ts-check +import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; +import * as node from '@warp-drive/internal-config/eslint/node.js'; +import * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs({ + files: ['fixtures/**/*.{js,ts}'], + }), + + // Test Support ================ + qunit.node({ + files: ['index.{js,ts}'], + }), +]; diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 9c7e854868d..26d281b8e3f 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -1,35 +1,35 @@ module.exports = { modules: [ + '@ember-data/active-record/request', '@ember-data/adapter', '@ember-data/adapter/error', '@ember-data/adapter/json-api', '@ember-data/adapter/rest', - '@ember-data/canary-features', '@ember-data/debug', - '@ember-data/deprecations', '@ember-data/experimental-preview-types', '@ember-data/graph', '@ember-data/json-api', + '@ember-data/json-api/request', '@ember-data/legacy-compat', '@ember-data/legacy-compat/builders', + '@ember-data/legacy-compat/utils', '@ember-data/model', '@ember-data/request', + '@ember-data/request-utils', '@ember-data/request/fetch', + '@ember-data/rest/request', '@ember-data/serializer', '@ember-data/serializer/json', '@ember-data/serializer/json-api', '@ember-data/serializer/rest', '@ember-data/store', '@ember-data/tracking', + '@warp-drive/build-config/canary-features', + '@warp-drive/build-config/debugging', + '@warp-drive/build-config/deprecations', 'ember-data-overview', ], classitems: [ - '(public) @ember-data/store RequestStateService#getLastRequestForRecord', - '(public) @ember-data/store RequestStateService#getPendingRequestsForRecord', - '(public) @ember-data/store RequestStateService#subscribeForRecord', - '(public) @ember-data/store IdentifierCache#getOrCreateDocumentIdentifier', - '(public) @ember-data/store Store#registerSchema', - '(public) @ember-data/store Store#schema', '(private) @ember-data/adapter BuildURLMixin#_buildURL', '(private) @ember-data/adapter BuildURLMixin#urlPrefix', '(private) @ember-data/adapter/json-api JSONAPIAdapter#ajaxOptions', @@ -51,34 +51,6 @@ module.exports = { '(private) @ember-data/debug InspectorDataAdapter#observeRecord', '(private) @ember-data/debug InspectorDataAdapter#watchModelTypes', '(private) @ember-data/debug InspectorDataAdapter#watchTypeIfUnseen', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_ARRAY_LIKE', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_A_USAGE', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_COMPUTED_CHAINS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_V1_RECORD_DATA', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_EARLY_STATIC', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_HAS_RECORD', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_HELPERS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_INSTANTIATE_RECORD_ARGS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_JSON_API_FALLBACK', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_MODEL_REOPEN', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_NON_EXPLICIT_POLYMORPHISM', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_PROMISE_PROXIES', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_RSVP_PROMISE', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_SAVE_PROMISE_ACCESS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_STORE_FIND', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_STRING_ARG_SCHEMAS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_V1CACHE_STORE_APIS', - '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_MANY_ARRAY_DUPLICATES_4_12', - '(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', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#queryRecord', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#saveRecord', '(private) @ember-data/legacy-compat SnapshotRecordArray#_recordArray', '(private) @ember-data/legacy-compat SnapshotRecordArray#_snapshots', '(private) @ember-data/legacy-compat SnapshotRecordArray#constructor', @@ -87,10 +59,9 @@ module.exports = { '(private) @ember-data/model Errors#errorsByAttributeName', '(private) @ember-data/model Errors#unknownProperty', '(private) @ember-data/model Model#_createSnapshot', - '(private) @ember-data/model Model#_debugInfo', - '(private) @ember-data/model Model#_notifyProperties', '(private) @ember-data/model Model#create', '(private) @ember-data/model Model#currentState', + '(private) @ember-data/model Model#determineRelationshipType', '(private) @ember-data/model PromiseManyArray#forEach', '(private) @ember-data/serializer/json JSONSerializer#_canSerialize', '(private) @ember-data/serializer/json JSONSerializer#_getMappedKey', @@ -122,6 +93,11 @@ module.exports = { '(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', + '(public) @ember-data/active-record/request @ember-data/active-record/request#findRecord', + '(public) @ember-data/active-record/request @ember-data/active-record/request#query', + '(public) @ember-data/active-record/request @ember-data/active-record/request#updateRecord', '(public) @ember-data/adapter Adapter#coalesceFindRequests', '(public) @ember-data/adapter Adapter#createRecord', '(public) @ember-data/adapter Adapter#deleteRecord', @@ -152,6 +128,7 @@ module.exports = { '(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', '(public) @ember-data/adapter/rest RESTAdapter#coalesceFindRequests', @@ -174,24 +151,15 @@ module.exports = { '(public) @ember-data/adapter/rest RESTAdapter#sortQueryParams', '(public) @ember-data/adapter/rest RESTAdapter#updateRecord', '(public) @ember-data/adapter/rest RESTAdapter#useFetch', - '(public) @ember-data/debug DebugLogging#LOG_GRAPH', - '(public) @ember-data/debug DebugLogging#LOG_IDENTIFIERS', - '(public) @ember-data/debug DebugLogging#LOG_INSTANCE_CACHE', - '(public) @ember-data/debug DebugLogging#LOG_MUTATIONS', - '(public) @ember-data/debug DebugLogging#LOG_NOTIFICATIONS', - '(public) @ember-data/debug DebugLogging#LOG_OPERATIONS', - '(public) @ember-data/debug DebugLogging#LOG_PAYLOADS', - '(public) @ember-data/debug DebugLogging#LOG_REQUESTS', - '(public) @ember-data/debug DebugLogging#LOG_REQUEST_STATUS', '(public) @ember-data/experimental-preview-types Adapter#coalesceFindRequests [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#createRecord', '(public) @ember-data/experimental-preview-types Adapter#deleteRecord', '(public) @ember-data/experimental-preview-types Adapter#destroy [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findAll', '(public) @ember-data/experimental-preview-types Adapter#findBelongsTo [OPTIONAL]', + '(public) @ember-data/experimental-preview-types Adapter#findhasMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findRecord', - '(public) @ember-data/experimental-preview-types Adapter#findhasMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#generateIdForRecord [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#groupRecordsForFindMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#query', @@ -202,6 +170,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Adapter#shouldReloadRecord [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#updateRecord', '(public) @ember-data/experimental-preview-types Cache#changedAttrs', + '(public) @ember-data/experimental-preview-types Cache#changedRelationships', '(public) @ember-data/experimental-preview-types Cache#clientDidCreate', '(public) @ember-data/experimental-preview-types Cache#commitWasRejected', '(public) @ember-data/experimental-preview-types Cache#didCommit', @@ -212,6 +181,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Cache#getErrors', '(public) @ember-data/experimental-preview-types Cache#getRelationship', '(public) @ember-data/experimental-preview-types Cache#hasChangedAttrs', + '(public) @ember-data/experimental-preview-types Cache#hasChangedRelationships', '(public) @ember-data/experimental-preview-types Cache#hydrate', '(public) @ember-data/experimental-preview-types Cache#isDeleted', '(public) @ember-data/experimental-preview-types Cache#isDeletionCommitted', @@ -224,6 +194,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Cache#peekRequest', '(public) @ember-data/experimental-preview-types Cache#put', '(public) @ember-data/experimental-preview-types Cache#rollbackAttrs', + '(public) @ember-data/experimental-preview-types Cache#rollbackRelationships', '(public) @ember-data/experimental-preview-types Cache#setAttr', '(public) @ember-data/experimental-preview-types Cache#setIsDeleted', '(public) @ember-data/experimental-preview-types Cache#unloadRecord', @@ -237,6 +208,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Serializer#serialize', '(public) @ember-data/experimental-preview-types Serializer#serializeIntoHash [OPTIONAL]', '(public) @ember-data/json-api Cache#changedAttrs', + '(public) @ember-data/json-api Cache#changedRelationships', '(public) @ember-data/json-api Cache#clientDidCreate', '(public) @ember-data/json-api Cache#commitWasRejected', '(public) @ember-data/json-api Cache#didCommit', @@ -246,6 +218,7 @@ module.exports = { '(public) @ember-data/json-api Cache#getErrors', '(public) @ember-data/json-api Cache#getRelationship', '(public) @ember-data/json-api Cache#hasChangedAttrs', + '(public) @ember-data/json-api Cache#hasChangedRelationships', '(public) @ember-data/json-api Cache#hydrate', '(public) @ember-data/json-api Cache#isDeleted', '(public) @ember-data/json-api Cache#isDeletionCommitted', @@ -258,23 +231,47 @@ module.exports = { '(public) @ember-data/json-api Cache#peekRequest', '(public) @ember-data/json-api Cache#put', '(public) @ember-data/json-api Cache#rollbackAttrs', + '(public) @ember-data/json-api Cache#rollbackRelationships', '(public) @ember-data/json-api Cache#setAttr', '(public) @ember-data/json-api Cache#setIsDeleted', '(public) @ember-data/json-api Cache#unloadRecord', '(public) @ember-data/json-api Cache#upsert', '(public) @ember-data/json-api Cache#version', '(public) @ember-data/json-api Cache#willCommit', + '(public) @ember-data/json-api/request @ember-data/json-api/request#buildQueryParams', + '(public) @ember-data/json-api/request @ember-data/json-api/request#createRecord', + '(public) @ember-data/json-api/request @ember-data/json-api/request#deleteRecord', + '(public) @ember-data/json-api/request @ember-data/json-api/request#findRecord', + '(public) @ember-data/json-api/request @ember-data/json-api/request#postQuery', + '(public) @ember-data/json-api/request @ember-data/json-api/request#query', + '(public) @ember-data/json-api/request @ember-data/json-api/request#serializePatch', + '(public) @ember-data/json-api/request @ember-data/json-api/request#serializeResources', + '(public) @ember-data/json-api/request @ember-data/json-api/request#setBuildURLConfig', + '(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord', '(public) @ember-data/legacy-compat SnapshotRecordArray#adapterOptions', '(public) @ember-data/legacy-compat SnapshotRecordArray#include', '(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', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#queryRecord', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#saveRecord', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureAssertFn', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureMismatchReporter', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureTypeNormalization', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedId', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedType', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivId', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivType', '(public) @ember-data/model @ember-data/model#attr', '(public) @ember-data/model @ember-data/model#belongsTo', '(public) @ember-data/model @ember-data/model#hasMany', '(public) @ember-data/model BelongsToReference#id', '(public) @ember-data/model BelongsToReference#identifier', + '(public) @ember-data/model BelongsToReference#key', '(public) @ember-data/model BelongsToReference#link', '(public) @ember-data/model BelongsToReference#links', '(public) @ember-data/model BelongsToReference#load', @@ -282,6 +279,7 @@ module.exports = { '(public) @ember-data/model BelongsToReference#push', '(public) @ember-data/model BelongsToReference#reload', '(public) @ember-data/model BelongsToReference#remoteType', + '(public) @ember-data/model BelongsToReference#type', '(public) @ember-data/model BelongsToReference#value', '(public) @ember-data/model BelongsToReference#value', '(public) @ember-data/model Errors#add', @@ -294,6 +292,7 @@ module.exports = { '(public) @ember-data/model Errors#remove', '(public) @ember-data/model HasManyReference#identifiers', '(public) @ember-data/model HasManyReference#ids', + '(public) @ember-data/model HasManyReference#key', '(public) @ember-data/model HasManyReference#link', '(public) @ember-data/model HasManyReference#links', '(public) @ember-data/model HasManyReference#load', @@ -301,6 +300,7 @@ module.exports = { '(public) @ember-data/model HasManyReference#push', '(public) @ember-data/model HasManyReference#reload', '(public) @ember-data/model HasManyReference#remoteType', + '(public) @ember-data/model HasManyReference#type', '(public) @ember-data/model HasManyReference#value', '(public) @ember-data/model Model#adapterError', '(public) @ember-data/model Model#attributes', @@ -353,14 +353,32 @@ module.exports = { '(public) @ember-data/model PromiseManyArray#meta', '(public) @ember-data/model PromiseManyArray#reload', '(public) @ember-data/model PromiseManyArray#then', - '(public) @ember-data/request RequestManager#request', - '(public) @ember-data/request RequestManager#use', - '(public) @ember-data/request RequestManager#useCache', + '(public) @ember-data/request CacheHandler#request', '(public) @ember-data/request Handler#request', '(public) @ember-data/request Future#abort', '(public) @ember-data/request Future#getStream', + '(public) @ember-data/request Future#id', + '(public) @ember-data/request Future#lid', '(public) @ember-data/request Future#onFinalize', - '(public) @ember-data/request CacheHandler#request', + '(public) @ember-data/request RequestManager#request', + '(public) @ember-data/request RequestManager#use', + '(public) @ember-data/request RequestManager#useCache', + '(public) @ember-data/request-utils @ember-data/request-utils#buildBaseURL', + '(public) @ember-data/request-utils @ember-data/request-utils#buildQueryParams', + '(public) @ember-data/request-utils @ember-data/request-utils#filterEmpty', + '(public) @ember-data/request-utils @ember-data/request-utils#parseCacheControl', + '(public) @ember-data/request-utils @ember-data/request-utils#setBuildURLConfig', + '(public) @ember-data/request-utils @ember-data/request-utils#sortQueryParams', + '(public) @ember-data/request-utils CachePolicy#didRequest', + '(public) @ember-data/request-utils CachePolicy#invalidateRequest', + '(public) @ember-data/request-utils CachePolicy#invalidateRequestsForType', + '(public) @ember-data/request-utils CachePolicy#isHardExpired', + '(public) @ember-data/request-utils CachePolicy#isSoftExpired', + '(public) @ember-data/rest/request @ember-data/rest/request#createRecord', + '(public) @ember-data/rest/request @ember-data/rest/request#deleteRecord', + '(public) @ember-data/rest/request @ember-data/rest/request#findRecord', + '(public) @ember-data/rest/request @ember-data/rest/request#query', + '(public) @ember-data/rest/request @ember-data/rest/request#updateRecord', '(public) @ember-data/serializer Serializer#normalize', '(public) @ember-data/serializer Serializer#normalizeResponse', '(public) @ember-data/serializer Serializer#serialize', @@ -429,9 +447,36 @@ module.exports = { '(public) @ember-data/store @ember-data/store#setIdentifierGenerationMethod', '(public) @ember-data/store @ember-data/store#setIdentifierResetMethod', '(public) @ember-data/store @ember-data/store#setIdentifierUpdateMethod', - '(public) @ember-data/store CacheManager#addToHasMany', - '(public) @ember-data/store CacheManager#changedAttributes', + '(public) @ember-data/store @ember-data/store#setKeyInfoForResource', + '(public) @ember-data/store CachePolicy#didRequest [Optional]', + '(public) @ember-data/store CachePolicy#isHardExpired', + '(public) @ember-data/store CachePolicy#isSoftExpired', + '(public) @ember-data/store CachePolicy#willRequest [Optional]', + '(public) @ember-data/store SchemaService#attributesDefinitionFor', + '(public) @ember-data/store SchemaService#derivation', + '(public) @ember-data/store SchemaService#doesTypeExist', + '(public) @ember-data/store SchemaService#fields', + '(public) @ember-data/store SchemaService#hashFn', + '(public) @ember-data/store SchemaService#hasResource', + '(public) @ember-data/store SchemaService#hasTrait', + '(public) @ember-data/store SchemaService#registerDerivations', + '(public) @ember-data/store SchemaService#registerHashFn', + '(public) @ember-data/store SchemaService#registerResource', + '(public) @ember-data/store SchemaService#registerResources', + '(public) @ember-data/store SchemaService#registerTransformations', + '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', + '(public) @ember-data/store SchemaService#resource', + '(public) @ember-data/store SchemaService#resourceHasTrait', + '(public) @ember-data/store SchemaService#transformation', + '(public) @ember-data/store CacheCapabilitiesManager#disconnectRecord', + '(public) @ember-data/store CacheCapabilitiesManager#getSchemaDefinitionService', + '(public) @ember-data/store CacheCapabilitiesManager#hasRecord', + '(public) @ember-data/store CacheCapabilitiesManager#identifierCache', + '(public) @ember-data/store CacheCapabilitiesManager#notifyChange', + '(public) @ember-data/store CacheCapabilitiesManager#schema', + '(public) @ember-data/store CacheCapabilitiesManager#setRecordId', '(public) @ember-data/store CacheManager#changedAttrs', + '(public) @ember-data/store CacheManager#changedRelationships', '(public) @ember-data/store CacheManager#clientDidCreate', '(public) @ember-data/store CacheManager#commitWasRejected', '(public) @ember-data/store CacheManager#didCommit', @@ -439,13 +484,10 @@ module.exports = { '(public) @ember-data/store CacheManager#dump', '(public) @ember-data/store CacheManager#fork', '(public) @ember-data/store CacheManager#getAttr', - '(public) @ember-data/store CacheManager#getBelongsTo', '(public) @ember-data/store CacheManager#getErrors', - '(public) @ember-data/store CacheManager#getHasMany', '(public) @ember-data/store CacheManager#getRelationship', - '(public) @ember-data/store CacheManager#getResourceIdentifier', - '(public) @ember-data/store CacheManager#hasChangedAttributes', '(public) @ember-data/store CacheManager#hasChangedAttrs', + '(public) @ember-data/store CacheManager#hasChangedRelationships', '(public) @ember-data/store CacheManager#hydrate', '(public) @ember-data/store CacheManager#isDeleted', '(public) @ember-data/store CacheManager#isDeletionCommitted', @@ -456,36 +498,28 @@ module.exports = { '(public) @ember-data/store CacheManager#patch', '(public) @ember-data/store CacheManager#peek', '(public) @ember-data/store CacheManager#peekRequest', - '(public) @ember-data/store CacheManager#pushData', '(public) @ember-data/store CacheManager#put', - '(public) @ember-data/store CacheManager#removeFromHasMany', - '(public) @ember-data/store CacheManager#rollbackAttributes', '(public) @ember-data/store CacheManager#rollbackAttrs', + '(public) @ember-data/store CacheManager#rollbackRelationships', '(public) @ember-data/store CacheManager#setAttr', - '(public) @ember-data/store CacheManager#setDirtyAttribute', - '(public) @ember-data/store CacheManager#setDirtyBelongsTo', - '(public) @ember-data/store CacheManager#setDirtyHasMany', '(public) @ember-data/store CacheManager#setIsDeleted', '(public) @ember-data/store CacheManager#unloadRecord', '(public) @ember-data/store CacheManager#upsert', '(public) @ember-data/store CacheManager#willCommit', - '(public) @ember-data/store CacheStoreWrapper#attributesDefinitionFor', - '(public) @ember-data/store CacheStoreWrapper#disconnectRecord', - '(public) @ember-data/store CacheStoreWrapper#getSchemaDefinitionService', - '(public) @ember-data/store CacheStoreWrapper#hasRecord', - '(public) @ember-data/store CacheStoreWrapper#identifierCache', - '(public) @ember-data/store CacheStoreWrapper#isRecordInUse', - '(public) @ember-data/store CacheStoreWrapper#notifyBelongsToChange', - '(public) @ember-data/store CacheStoreWrapper#notifyChange', - '(public) @ember-data/store CacheStoreWrapper#notifyErrorsChange', - '(public) @ember-data/store CacheStoreWrapper#notifyHasManyChange', - '(public) @ember-data/store CacheStoreWrapper#notifyPropertyChange', - '(public) @ember-data/store CacheStoreWrapper#notifyStateChange', - '(public) @ember-data/store CacheStoreWrapper#recordDataFor', - '(public) @ember-data/store CacheStoreWrapper#relationshipsDefinitionFor', - '(public) @ember-data/store CacheStoreWrapper#setRecordId', + '(public) @ember-data/store Document#data', + '(public) @ember-data/store Document#errors', + '(public) @ember-data/store Document#fetch', + '(public) @ember-data/store Document#first', + '(public) @ember-data/store Document#identifier', + '(public) @ember-data/store Document#last', + '(public) @ember-data/store Document#links', + '(public) @ember-data/store Document#meta', + '(public) @ember-data/store Document#next', + '(public) @ember-data/store Document#prev', + '(public) @ember-data/store Document#toJSON', '(public) @ember-data/store IdentifierCache#createIdentifierForNewRecord', '(public) @ember-data/store IdentifierCache#forgetRecordIdentifier', + '(public) @ember-data/store IdentifierCache#getOrCreateDocumentIdentifier', '(public) @ember-data/store IdentifierCache#getOrCreateRecordIdentifier', '(public) @ember-data/store IdentifierCache#updateRecordIdentifier', '(public) @ember-data/store ManyArray#createRecord', @@ -508,9 +542,9 @@ module.exports = { '(public) @ember-data/store RecordReference#reload', '(public) @ember-data/store RecordReference#remoteType', '(public) @ember-data/store RecordReference#value', - '(public) @ember-data/store SchemaService#attributesDefinitionFor', - '(public) @ember-data/store SchemaService#doesTypeExist', - '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', + '(public) @ember-data/store RequestStateService#getLastRequestForRecord', + '(public) @ember-data/store RequestStateService#getPendingRequestsForRecord', + '(public) @ember-data/store RequestStateService#subscribeForRecord', '(public) @ember-data/store Snapshot#adapterOptions', '(public) @ember-data/store Snapshot#attr', '(public) @ember-data/store Snapshot#attributes', @@ -533,7 +567,6 @@ module.exports = { '(public) @ember-data/store Store#cache', '(public) @ember-data/store Store#createCache (hook)', '(public) @ember-data/store Store#createRecord', - '(public) @ember-data/store Store#createRecordDataFor (hook)', '(public) @ember-data/store Store#deleteRecord', '(public) @ember-data/store Store#findAll', '(public) @ember-data/store Store#findRecord', @@ -554,10 +587,12 @@ module.exports = { '(public) @ember-data/store Store#pushPayload', '(public) @ember-data/store Store#query', '(public) @ember-data/store Store#queryRecord', + '(public) @ember-data/store Store#registerSchema', '(public) @ember-data/store Store#registerSchemaDefinitionService', '(public) @ember-data/store Store#request', '(public) @ember-data/store Store#requestManager', '(public) @ember-data/store Store#saveRecord', + '(public) @ember-data/store Store#schema', '(public) @ember-data/store Store#serializerFor', '(public) @ember-data/store Store#teardownRecord (hook)', '(public) @ember-data/store Store#unloadAll', @@ -565,5 +600,42 @@ module.exports = { '(public) @ember-data/tracking @ember-data/tracking#memoTransact', '(public) @ember-data/tracking @ember-data/tracking#transact', '(public) @ember-data/tracking @ember-data/tracking#untracked', + '(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_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_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_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_NON_STRICT_ID', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_TYPES', + '(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#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/index.js b/tests/docs/index.js index 3ae88a6abbf..733712a8d67 100644 --- a/tests/docs/index.js +++ b/tests/docs/index.js @@ -10,12 +10,6 @@ function isNonEmptyString(str) { return typeof str === 'string' && str.length > 0; } function isOwnModule(item) { - if (item.module) { - return ['ember-inflector'].indexOf(item.module) === -1; - } - if (item.class) { - return ['Ember.Inflector', 'Ember.HTMLBars.helpers'].indexOf(item.class) === -1; - } return item.file.indexOf('node_modules') === -1; } @@ -24,8 +18,8 @@ function linkItem(item) { } QUnit.module('Docs coverage', function (hooks) { - // data.json is generated and not always present. So this disable needs to be preserved. - const docs = require('../../packages/-ember-data/dist/docs/data.json'); // eslint-disable-line node/no-missing-require + const docsStr = fs.readFileSync('../../packages/-ember-data/dist/docs/data.json', 'utf8'); + const docs = JSON.parse(docsStr); const expected = require('./fixtures/expected'); function classIsPublic(className) { @@ -63,7 +57,7 @@ QUnit.module('Docs coverage', function (hooks) { assert.ok(docs.files[def.file], `${className} has a file`); assert.true( def.access === 'public' || def.access === 'private', - `${def.name} must declare either as either @internal @private or @public` + `${def.name} must declare either as either @typedoc @internal @private or @public` ); if (def.access !== 'private') { assert.true(isNonEmptyString(def.description), `${className} must provide a description.`); @@ -84,7 +78,7 @@ QUnit.module('Docs coverage', function (hooks) { } // docs without a private flag are published as public by default // We error for these - let status = item.access || 'public'; + const status = item.access || 'public'; return `(${status}) ${item.module ? `${item.module} ` : ''}${item.class}#${item.name}`; }) .filter(Boolean) @@ -123,7 +117,7 @@ QUnit.module('Docs coverage', function (hooks) { } has a complete definition`, function (assert) { assert.true( item.access === 'public' || item.access === 'private', - `${item.name} must declare either as either @internal @private or @public in ${linkItem(item)}` + `${item.name} must declare either as either @typedoc @internal @private or @public in ${linkItem(item)}` ); assert.true( item.access === 'private' || (item.class && classIsPublic(item.class)), @@ -140,7 +134,7 @@ QUnit.module('Docs coverage', function (hooks) { }); test('No missing classitems', function (assert) { - let missing = setDifference(expectedItems, docsItems); + const missing = setDifference(expectedItems, docsItems); assert.emptySet( missing, 'If you intentionally removed a public API method, please udpate tests/docs/expected.js. Otherwise, documentation is missing, incorrectly formatted, or in a directory that is not watched by yuidoc. All files containing documentation must have a yuidoc class declaration.' @@ -148,7 +142,7 @@ QUnit.module('Docs coverage', function (hooks) { }); test('No extraneous classitems', function (assert) { - let extraneous = setDifference(docsItems, expectedItems); + const extraneous = setDifference(docsItems, expectedItems); assert.emptySet( extraneous, 'If you have added new features, please update tests/docs/expected.js and confirm that any public properties are marked both @public and @static to be included in the Ember API Docs viewer.' @@ -168,7 +162,7 @@ QUnit.module('Docs coverage', function (hooks) { }); function setDifference(setA, setB) { - let difference = new Set(setA); + const difference = new Set(setA); for (var elem of setB) { difference.delete(elem); } diff --git a/tests/docs/package.json b/tests/docs/package.json index c7231609cf0..1566bdc5dbf 100644 --- a/tests/docs/package.json +++ b/tests/docs/package.json @@ -11,16 +11,21 @@ "license": "MIT", "author": "", "scripts": { - "test": "qunit ./index.js" + "test:docs": "qunit ./index.js", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "devDependencies": { - "qunit": "^2.19.4" + "@warp-drive/internal-config": "workspace:*", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "qunit": "^2.20.1" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "dependencies": {} } diff --git a/tests/ember-data__adapter/README.md b/tests/ember-data__adapter/README.md new file mode 100644 index 00000000000..35beb9fcf76 --- /dev/null +++ b/tests/ember-data__adapter/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/adapter + +This test-package provides tests for the `@ember-data/adapter` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__adapter; +pnpm test; +``` diff --git a/tests/adapter-encapsulation/app/app.js b/tests/ember-data__adapter/app/app.js similarity index 100% rename from tests/adapter-encapsulation/app/app.js rename to tests/ember-data__adapter/app/app.js diff --git a/tests/ember-data__adapter/app/index.html b/tests/ember-data__adapter/app/index.html new file mode 100644 index 00000000000..1e808480500 --- /dev/null +++ b/tests/ember-data__adapter/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/adapter Tests + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/packages/unpublished-test-infra/tests/dummy/app/resolver.js b/tests/ember-data__adapter/app/resolver.js similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/app/resolver.js rename to tests/ember-data__adapter/app/resolver.js diff --git a/tests/adapter-encapsulation/app/router.js b/tests/ember-data__adapter/app/router.js similarity index 100% rename from tests/adapter-encapsulation/app/router.js rename to tests/ember-data__adapter/app/router.js diff --git a/tests/ember-data__adapter/app/services/store.ts b/tests/ember-data__adapter/app/services/store.ts new file mode 100644 index 00000000000..2e360e8abb2 --- /dev/null +++ b/tests/ember-data__adapter/app/services/store.ts @@ -0,0 +1,60 @@ +import JSONAPICache from '@ember-data/json-api'; +import { + adapterFor, + cleanup, + LegacyNetworkHandler, + normalize, + pushPayload, + serializeRecord, + serializerFor, +} from '@ember-data/legacy-compat'; +import type Model from '@ember-data/model'; +import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember-data/model/hooks'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import BaseStore, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; + +export default class Store extends BaseStore { + constructor(args: unknown) { + super(args); + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager) { + return new JSONAPICache(capabilities); + } + + override instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: Record): Model { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model) { + teardownRecord.call(this, record); + } + + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { + return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type); + } + + serializeRecord = serializeRecord; + pushPayload = pushPayload; + adapterFor = adapterFor; + serializerFor = serializerFor; + normalize = normalize; + + override destroy() { + cleanup.call(this); + super.destroy(); + } +} diff --git a/tests/debug-encapsulation/app/styles/app.css b/tests/ember-data__adapter/app/styles/app.css similarity index 100% rename from tests/debug-encapsulation/app/styles/app.css rename to tests/ember-data__adapter/app/styles/app.css diff --git a/packages/unpublished-test-infra/tests/dummy/app/templates/application.hbs b/tests/ember-data__adapter/app/templates/application.hbs similarity index 100% rename from packages/unpublished-test-infra/tests/dummy/app/templates/application.hbs rename to tests/ember-data__adapter/app/templates/application.hbs diff --git a/tests/ember-data__adapter/config/environment.js b/tests/ember-data__adapter/config/environment.js new file mode 100644 index 00000000000..68a704ce567 --- /dev/null +++ b/tests/ember-data__adapter/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__adapter', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/adapter-encapsulation/config/optional-features.json b/tests/ember-data__adapter/config/optional-features.json similarity index 100% rename from tests/adapter-encapsulation/config/optional-features.json rename to tests/ember-data__adapter/config/optional-features.json diff --git a/tests/adapter-encapsulation/config/targets.js b/tests/ember-data__adapter/config/targets.js similarity index 100% rename from tests/adapter-encapsulation/config/targets.js rename to tests/ember-data__adapter/config/targets.js diff --git a/tests/ember-data__adapter/diagnostic.js b/tests/ember-data__adapter/diagnostic.js new file mode 100644 index 00000000000..ede75dbb1ad --- /dev/null +++ b/tests/ember-data__adapter/diagnostic.js @@ -0,0 +1,3 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; + +await launch(); diff --git a/tests/ember-data__adapter/ember-cli-build.js b/tests/ember-data__adapter/ember-cli-build.js new file mode 100644 index 00000000000..436ede741f6 --- /dev/null +++ b/tests/ember-data__adapter/ember-cli-build.js @@ -0,0 +1,28 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); + + return app.toTree(); +}; diff --git a/tests/ember-data__adapter/eslint.config.mjs b/tests/ember-data__adapter/eslint.config.mjs new file mode 100644 index 00000000000..5f9f29b44b5 --- /dev/null +++ b/tests/ember-data__adapter/eslint.config.mjs @@ -0,0 +1,36 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts'], + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + diagnostic.browser({ + allowedImports: ['@ember/object'], + }), +]; diff --git a/tests/ember-data__adapter/package.json b/tests/ember-data__adapter/package.json new file mode 100644 index 00000000000..63929ba6a6b --- /dev/null +++ b/tests/ember-data__adapter/package.json @@ -0,0 +1,123 @@ +{ + "name": "ember-data__adapter", + "version": "4.12.8", + "private": true, + "description": "Tests for @ember-data/adapter", + "repository": { + "type": "git", + "url": "https://github.com/emberjs/data.git", + "directory": "tests/ember-data__adapter" + }, + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "_build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "test": "bun ./diagnostic.js", + "_test:production": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@ember-data/debug": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/serializer": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@warp-drive/diagnostic": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-rive/build-config": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + } + }, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.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.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-test-loader": "^3.1.0", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "loader.js": "^4.7.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "engines": { + "node": ">= 18.20.4" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9" +} diff --git a/tests/ember-data__adapter/tests/index.html b/tests/ember-data__adapter/tests/index.html new file mode 100644 index 00000000000..06fe27520dd --- /dev/null +++ b/tests/ember-data__adapter/tests/index.html @@ -0,0 +1,36 @@ + + + + + + @ember-data/adapter Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+ + + + + + + {{content-for "body-footer"}} + + + diff --git a/tests/ember-data__adapter/tests/integration/belongs-to-test.js b/tests/ember-data__adapter/tests/integration/belongs-to-test.js new file mode 100644 index 00000000000..ae734152b0d --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/belongs-to-test.js @@ -0,0 +1,527 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + const json = { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + relationships: {}, + }, + }; + + snapshot.eachRelationship((key, relationship) => { + if (relationship.kind === 'belongsTo') { + this.serializeBelongsTo(snapshot, json, relationship); + } else if (relationship.kind === 'hasMany') { + this.serializeHasMany(snapshot, json, relationship); + } + }); + + if (Object.keys(json.data.relationships).length === 0) { + delete json.data.relationships; + } + + return json; + } + + // minimal implementation, not json-api compliant + serializeBelongsTo(snapshot, json, relationship) { + const key = relationship.name; + const belongsTo = snapshot.belongsTo(key); + + if (belongsTo) { + const value = { + data: { + id: belongsTo.id, + type: belongsTo.modelName, + }, + }; + json.data.relationships[key] = value; + } + } + + // minimal implementation, not json-api compliant + serializeHasMany(snapshot, json, relationship) { + const key = relationship.name; + const hasMany = snapshot.hasMany(key); + + if (hasMany && hasMany.length) { + const value = { + data: hasMany.map((snap) => ({ + id: snap.id, + type: snap.modelName, + })), + }; + json.data.relationships[key] = value; + } + } +} + +class Post extends Model { + @attr + text; + + @hasMany('comments', { async: true, inverse: 'post' }) + comments; +} + +class Comment extends Model { + @attr + text; + + @belongsTo('post', { async: true, inverse: 'comments' }) + post; +} + +module('integration/belongs-to - Belongs To Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + }); + + test('if a belongsTo relationship has a link but no data (findBelongsTo is defined)', async function (assert) { + let findRecordCalled = 0; + let findBelongsToCalled = 0; + + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + links: { + related: 'https://example.com/api/post/2', + }, + }, + }, + }, + }; + + const expectedResult = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindBelongsToAdapter extends EmberObject { + findRecord() { + findRecordCalled++; + } + + findBelongsTo(passedStore, snapshot, url, relationship) { + findBelongsToCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findBelongsTo'); + + const expectedURL = initialRecord.data.relationships.post.links.related; + assert.equal(url, expectedURL, 'url is passed to findBelongsTo'); + assert.equal(relationship.name, 'post', 'relationship is passed to findBelongsTo'); + + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findBelongsTo with correct modelName'); + assert.equal(snapshot.id, '3', 'snapshot is passed to findBelongsTo with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindBelongsToAdapter); + + const comment = store.push(initialRecord); + + const post = await comment.post; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findBelongsToCalled, 1, 'findBelongsTo is called once'); + assert.deepEqual(post.serialize(), expectedResult, 'findBelongsTo returns expected result'); + }); + + testInDebug( + 'if a belongsTo relationship has a link but no data (findBelongsTo is undefined)', + async function (assert) { + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + links: { + related: 'https://example.com/api/post/2', + }, + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestFindBelongsToAdapter extends EmberObject {} + + owner.register('adapter:application', TestFindBelongsToAdapter); + + const comment = store.push(initialRecord); + + await assert.expectAssertion(async function () { + await comment.post; + }, /You tried to load a belongsTo relationship from a specified 'link' in the original payload but your adapter does not implement 'findBelongsTo'/); + } + ); + + test('if a belongsTo relationship has data but not a link (findBelongsTo is defined)', async function (assert) { + let findRecordCalled = 0; + let findBelongsToCalled = 0; + + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + }, + }, + }, + }; + + const expectedResult = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + findRecord(passedStore, type, id, snapshot) { + findRecordCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Post, 'model is passed to findRecord'); + assert.equal(id, '2', 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultCopy); + } + + findBelongsTo() { + findBelongsToCalled++; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const comment = store.push(initialRecord); + + const post = await comment.post; + + assert.equal(findRecordCalled, 1, 'findRecord is called once'); + assert.equal(findBelongsToCalled, 0, 'findBelongsTo is not called'); + assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); + }); + + test('if a belongsTo relationship has data but not a link (findBelongsTo is not defined)', async function (assert) { + let findRecordCalled = 0; + + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + }, + }, + }, + }; + + const expectedResult = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + findRecord(passedStore, type, id, snapshot) { + findRecordCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Post, 'model is passed to findRecord'); + assert.equal(id, '2', 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const comment = store.push(initialRecord); + + const post = await comment.post; + + assert.equal(findRecordCalled, 1, 'findRecord is called once'); + assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); + }); + + test('if a belongsTo relationship has a link and data (findBelongsTo is defined)', async function (assert) { + let findRecordCalled = 0; + let findBelongsToCalled = 0; + + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + links: { + related: 'https://example.com/api/post/2', + }, + }, + }, + }, + }; + + const expectedResult = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindBelongsToAdapter extends EmberObject { + findRecord() { + findRecordCalled++; + } + + findBelongsTo(passedStore, snapshot, url, relationship) { + findBelongsToCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findBelongsTo'); + + const expectedURL = initialRecord.data.relationships.post.links.related; + assert.equal(url, expectedURL, 'url is passed to findBelongsTo'); + assert.equal(relationship.name, 'post', 'relationship is passed to findBelongsTo'); + + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findBelongsTo with correct modelName'); + assert.equal(snapshot.id, '3', 'snapshot is passed to findBelongsTo with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindBelongsToAdapter); + + const comment = store.push(initialRecord); + + const post = await comment.post; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findBelongsToCalled, 1, 'findBelongsTo is called once'); + assert.deepEqual(post.serialize(), expectedResult, 'findBelongsTo returns expected result'); + }); + + test('if a belongsTo relationship has link and data (findBelongsTo is not defined)', async function (assert) { + let findRecordCalled = 0; + + const initialRecord = { + data: { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + }, + }, + }, + }; + + const expectedResult = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + findRecord(passedStore, type, id, snapshot) { + findRecordCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Post, 'model is passed to findRecord'); + assert.equal(id, '2', 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'post', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, '2', 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const comment = store.push(initialRecord); + + const post = await comment.post; + + assert.equal(findRecordCalled, 1, 'findRecord is called once'); + assert.deepEqual(post.serialize(), expectedResult, 'findRecord returns expected result'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/coalescing-test.js b/tests/ember-data__adapter/tests/integration/coalescing-test.js new file mode 100644 index 00000000000..7577468a145 --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/coalescing-test.js @@ -0,0 +1,751 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + return { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + }, + }; + } +} + +class Person extends Model { + @attr + firstName; + + @attr + lastName; +} + +module('integration/coalescing - Coalescing Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:person', Person); + }); + + test('coalesceFindRequests is true and findMany is not defined', async function (assert) { + let findRecordCalled = 0; + + const expectedResults = [ + { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }, + { + data: { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + }, + ]; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultsCopy = structuredClone(expectedResults); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord(passedStore, type, id, snapshot) { + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Person, 'model is passed to findRecord'); + + const expectedId = expectedResultsCopy[findRecordCalled].data.id; + assert.equal(id, expectedId, 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultsCopy[findRecordCalled++]); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const promises = expectedResults.map((result) => result.data.id).map((id) => store.findRecord('person', id)); + const records = await Promise.all(promises); + + const serializedRecords = records.map((record) => record.serialize()); + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); + }); + + test('coalesceFindRequests is true and findMany is defined', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let groupRecordsForFindManyCalled = 0; + + let expectedResults = { + data: [ + { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + ], + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultsCopy = structuredClone(expectedResults); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Person, 'model is passed to findMany'); + + const expectedIds = expectedResultsCopy.data.map((record) => record.id); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResultsCopy); + } + + groupRecordsForFindMany(store, snapshots) { + groupRecordsForFindManyCalled++; + return [snapshots]; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); + const records = await Promise.all(promises); + + const serializedRecords = records.slice().map((record) => record.serialize()); + expectedResults = expectedResults.data.map((result) => ({ data: result })); + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.equal(groupRecordsForFindManyCalled, 1, 'groupRecordsForFindMany is called once'); + assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); + }); + + test('Coalescing works with multiple includes options specified (bypass findMany)', async function (assert) { + let findRecordCalled = 0; + + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord(_store, _schema, id, snapshot) { + findRecordCalled++; + + return { + data: + id === '1' + ? { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + } + : { + id: '2', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + }; + } + + findMany() { + throw new Error(`We should not call findMany`); + } + + groupRecordsForFindMany() { + throw new Error(`We should not call groupRecordForFindMany`); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const person1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const person2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '2' }); + const promises = [ + store.findRecord('person', '1'), // creates request (1) + store.findRecord('person', '1', { include: '' }), // de-duped + store.findRecord('person', '1', { include: 'users' }), // creates request (2) + store.findRecord('person', '1', { include: 'users', adapterOptions: { opt: '1' } }), // creates request (3) + store.findRecord('person', '1', { include: 'users', adapterOptions: { opt: '2' } }), // creates request (4) + store.findRecord('person', '1', { include: 'users' }), // de-duped + store.findRecord('person', '1', { adapterOptions: { opt: '2' } }), // creates request (5) + store.findRecord('person', '1', { include: 'users.foo' }), // creates request (6) + store.findRecord('person', '2', { include: 'users.foo' }), // creates request (7) + store.findRecord('person', '2', { include: 'users' }), // creates request (8) + store.findRecord('person', '2', { include: 'users' }), // de-duped + store.findRecord('person', '2', { include: '' }), // de-duped + store.findRecord('person', '2'), // de-duped + store.findRecord('person', '2', { include: 'users' }), // de-duped + store.findRecord('person', '2', { include: 'users.foo' }), // de-duped + ]; + const records = await Promise.all(promises); + const foundIdentifiers = records.map((record) => recordIdentifierFor(record)); + const expectedIdentifiers = [ + person1, + person1, + person1, + person1, + person1, + person1, + person1, + person1, + person2, + person2, + person2, + person2, + person2, + person2, + person2, + ]; + + assert.equal(findRecordCalled, 8, 'findRecord is called 8x'); + assert.deepEqual(foundIdentifiers, expectedIdentifiers, 'each findRecord returns expected result'); + + const person1record = store.peekRecord('person', '1'); + const person2record = store.peekRecord('person', '2'); + assert.equal(person1record.firstName, 'Gaurav', 'person 1 loaded'); + assert.equal(person2record.firstName, 'Chris', 'person 2 loaded'); + }); + + test('Coalescing works with multiple includes options specified (uses findMany)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let groupRecordsForFindManyCalled = 0; + + let expectedResults = { + data: [ + { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + { + id: '2', + type: 'person', + attributes: { + firstName: 'Wesley', + lastName: 'Thoburn', + }, + }, + { + id: '3', + type: 'person', + attributes: { + firstName: 'James', + lastName: 'Thoburn', + }, + }, + { + id: '4', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + ], + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + + return { data: null }; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Person, 'model is passed to findMany'); + + const expectedIds = ['1', '2', '3', '4']; + const expectedIncludes = [undefined, 'users', 'users.foo', ['comments']]; + const expectedOptions = [undefined, undefined, { opt: '1' }, { opt: '2' }]; + const includes = snapshots.map((snapshot) => snapshot.include); + const options = snapshots.map((snapshot) => snapshot.adapterOptions); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + assert.deepEqual(includes, expectedIncludes, 'includes are what was expected'); + assert.deepEqual(options, expectedOptions, 'options are what was expected'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResults); + } + + groupRecordsForFindMany(_store, snapshots) { + groupRecordsForFindManyCalled++; + return [snapshots]; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const person1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const person2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '2' }); + const person3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '3' }); + const person4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '4' }); + const promises = [ + store.findRecord('person', '1'), + store.findRecord('person', '2', { include: 'users' }), + store.findRecord('person', '3', { include: 'users.foo', adapterOptions: { opt: '1' } }), + store.findRecord('person', '4', { include: ['comments'], adapterOptions: { opt: '2' } }), + ]; + const records = await Promise.all(promises); + const foundIdentifiers = records.map((record) => recordIdentifierFor(record)); + const expectedIdentifiers = [person1, person2, person3, person4]; + expectedResults = expectedResults.data.map((result) => ({ data: result })); + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.equal(groupRecordsForFindManyCalled, 1, 'groupRecordsForFindMany is called once'); + assert.deepEqual(foundIdentifiers, expectedIdentifiers, 'each findRecord returns expected result'); + + const person1record = store.peekRecord('person', '1'); + const person2record = store.peekRecord('person', '2'); + const person3record = store.peekRecord('person', '3'); + const person4record = store.peekRecord('person', '4'); + assert.equal(person1record.firstName, 'Gaurav', 'person 1 loaded'); + assert.equal(person2record.firstName, 'Wesley', 'person 2 loaded'); + assert.equal(person3record.firstName, 'James', 'person 3 loaded'); + assert.equal(person4record.firstName, 'Chris', 'person 4 loaded'); + }); + + test('coalesceFindRequests is true and findMany is defined but groupRecordsForFindMany is undefined', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + + let expectedResults = { + data: [ + { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + ], + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + const expectedResultsCopy = structuredClone(expectedResults); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Person, 'model is passed to findMany'); + + const expectedIds = expectedResultsCopy.data.map((record) => record.id); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResultsCopy); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const promises = expectedResults.data.map((result) => result.id).map((id) => store.findRecord('person', id)); + const records = await Promise.all(promises); + + const serializedRecords = records.slice().map((record) => record.serialize()); + expectedResults = expectedResults.data.map((result) => ({ data: result })); + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); + }); + + test('coalesceFindRequests is false', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let groupRecordsForFindManyCalled = 0; + + const expectedResults = [ + { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }, + { + data: { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + }, + ]; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultsCopy = structuredClone(expectedResults); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = false; + + findRecord(passedStore, type, id, snapshot) { + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Person, 'model is passed to findRecord'); + + const expectedId = expectedResultsCopy[findRecordCalled].data.id; + assert.equal(id, expectedId, 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultsCopy[findRecordCalled++]); + } + + findMany() { + findManyCalled++; + } + + groupRecordsForFindMany(store, snapshots) { + groupRecordsForFindManyCalled++; + return [snapshots]; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const promises = expectedResults.map((result) => result.data.id).map((id) => store.findRecord('person', id)); + const records = await Promise.all(promises); + + const serializedRecords = records.map((record) => record.serialize()); + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.equal(groupRecordsForFindManyCalled, 0, 'groupRecordsForFindMany is not called'); + assert.deepEqual(serializedRecords, expectedResults, 'each findRecord returns expected result'); + }); + + test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord', async function (assert) { + let findRecordCalled = 0; + + const { owner } = this; + const store = owner.lookup('service:store'); + const options = [ + undefined, // not de-duped since is first request seen + { reload: true }, // de-dupe + { backgroundReload: true }, // de-dupe + { reload: true, include: 'comments,friends' }, // should produce a request + { reload: true, include: ['friends', 'comments'] }, // de-dupe + { reload: true, include: 'friends,comments' }, // de-dupe + { reload: true, include: 'notFriends,comments' }, // should produce a request + { reload: true, include: 'comments' }, // de-dupe since included in comments,friends + { reload: true, include: 'friends' }, // de-dupe since included in comments,friends + ]; + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + async findRecord(_store, _schema, _id, snapshot) { + findRecordCalled++; + + if (findRecordCalled === 1) { + assert.equal(snapshot.include, undefined, 'No include for first request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for first request'); + } else if (findRecordCalled === 2) { + assert.equal(snapshot.include, 'comments,friends', 'include is correct for second request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); + } else if (findRecordCalled === 3) { + assert.equal(snapshot.include, 'notFriends,comments', 'include is correct for third request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); + } + + return { + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + } + + async findMany() { + throw new Error(`Expected findMany to not be called`); + } + + groupRecordsForFindMany() { + throw new Error(`Expected groupRecordsForFindMany to not be called`); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const request = store.findRecord('person', '1', options[0]); + const request2 = store.findRecord('person', '1', options[1]); + const request3 = store.findRecord('person', '1', options[2]); + const request4 = store.findRecord('person', '1', options[3]); + const request5 = store.findRecord('person', '1', options[4]); + const request6 = store.findRecord('person', '1', options[5]); + const request7 = store.findRecord('person', '1', options[6]); + const request8 = store.findRecord('person', '1', options[7]); + const request9 = store.findRecord('person', '1', options[8]); + + await Promise.all([request, request2, request3, request4, request5, request6, request7, request8, request9]); + + assert.equal(store.peekAll('person').length, 1, 'only one record is in the store'); + + assert.equal(findRecordCalled, 3, 'findRecord is called three times'); + }); + + test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord (order scenario 2)', async function (assert) { + let findRecordCalled = 0; + + const { owner } = this; + const store = owner.lookup('service:store'); + const options = [ + { include: 'comments' }, // not de-duped since first request + { reload: true, include: 'comments,friends' }, // should produce a request + undefined, // de-dupe + { reload: true }, // de-dupe + { backgroundReload: true }, // de-dupe + { reload: true, include: ['friends', 'comments'] }, // de-dupe + { reload: true, include: 'friends,comments' }, // de-dupe + { reload: true, include: 'notFriends,comments' }, // should produce a request + { reload: true, include: 'friends' }, // de-dupe since included in comments,friends + ]; + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + async findRecord(_store, _schema, _id, snapshot) { + findRecordCalled++; + + if (findRecordCalled === 1) { + assert.equal(snapshot.include, 'comments', 'include for first request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for first request'); + } else if (findRecordCalled === 2) { + assert.equal(snapshot.include, 'comments,friends', 'include is correct for second request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); + } else if (findRecordCalled === 3) { + assert.equal(snapshot.include, 'notFriends,comments', 'include is correct for third request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); + } + + return { + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + } + + async findMany() { + throw new Error(`Expected findMany to not be called`); + } + + groupRecordsForFindMany() { + throw new Error(`Expected groupRecordsForFindMany to not be called`); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const request = store.findRecord('person', '1', options[0]); + const request2 = store.findRecord('person', '1', options[1]); + const request3 = store.findRecord('person', '1', options[2]); + const request4 = store.findRecord('person', '1', options[3]); + const request5 = store.findRecord('person', '1', options[4]); + const request6 = store.findRecord('person', '1', options[5]); + const request7 = store.findRecord('person', '1', options[6]); + const request8 = store.findRecord('person', '1', options[7]); + const request9 = store.findRecord('person', '1', options[8]); + + await Promise.all([request, request2, request3, request4, request5, request6, request7, request8, request9]); + + assert.equal(store.peekAll('person').length, 1, 'only one record is in the store'); + + assert.equal(findRecordCalled, 3, 'findRecord is called three times'); + }); + + test('Coalescing accounts for multiple findRecord calls with different options by de-duping and using findRecord (order scenario 3)', async function (assert) { + let findRecordCalled = 0; + + const { owner } = this; + const store = owner.lookup('service:store'); + const options = [ + { reload: true, include: 'comments,friends' }, // not de-duped since first request + undefined, // de-dupe + { reload: true }, // de-dupe + { backgroundReload: true }, // de-dupe + { reload: true, include: ['friends', 'comments'] }, // de-dupe + { reload: true, include: 'friends,comments' }, // de-dupe + { reload: true, include: 'notFriends,comments' }, // should produce a request + { include: 'comments' }, // de-dupe + { reload: true, include: 'friends' }, // de-dupe since included in comments,friends + ]; + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = true; + + async findRecord(_store, _schema, _id, snapshot) { + findRecordCalled++; + + if (findRecordCalled === 1) { + assert.equal(snapshot.include, 'comments,friends', 'include is correct for second request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for second request'); + } else if (findRecordCalled === 2) { + assert.equal(snapshot.include, 'notFriends,comments', 'include is correct for third request'); + assert.equal(snapshot.adapterOptions, undefined, 'No adapterOptions for third request'); + } + + return { + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + } + + async findMany() { + throw new Error(`Expected findMany to not be called`); + } + + groupRecordsForFindMany() { + throw new Error(`Expected groupRecordsForFindMany to not be called`); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const request = store.findRecord('person', '1', options[0]); + const request2 = store.findRecord('person', '1', options[1]); + const request3 = store.findRecord('person', '1', options[2]); + const request4 = store.findRecord('person', '1', options[3]); + const request5 = store.findRecord('person', '1', options[4]); + const request6 = store.findRecord('person', '1', options[5]); + const request7 = store.findRecord('person', '1', options[6]); + const request8 = store.findRecord('person', '1', options[7]); + const request9 = store.findRecord('person', '1', options[8]); + + await Promise.all([request, request2, request3, request4, request5, request6, request7, request8, request9]); + + assert.equal(store.peekAll('person').length, 1, 'only one record is in the store'); + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/generate-id-test.js b/tests/ember-data__adapter/tests/integration/generate-id-test.js new file mode 100644 index 00000000000..6b63182692f --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/generate-id-test.js @@ -0,0 +1,118 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr } from '@ember-data/model'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + return { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + }, + }; + } +} + +class Person extends Model { + @attr + firstName; + + @attr + lastName; +} + +module('integration/generate-id - GenerateIdForRecord Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:person', Person); + }); + + test('store.createRecord calls adapter.generateIdForRecord if defined and we use this ID for the record', async function (assert) { + let generateIdForRecordCalled = 0; + let seq = 0; + + const store = this.owner.lookup('service:store'); + const expectedProps = { + firstName: 'Gaurav', + lastName: 'Munjal', + }; + + class TestGenerateIdForRecordAdapter extends EmberObject { + generateIdForRecord() { + generateIdForRecordCalled++; + + return 'manually generated id ' + ++seq; + } + } + + this.owner.register('adapter:application', TestGenerateIdForRecordAdapter); + + const record = store.createRecord('person', expectedProps); + + assert.equal(record.id, 'manually generated id 1', 'manually generated id used'); + + const recordFromPeekRecord = store.peekRecord('person', record.id); + + assert.equal(record, recordFromPeekRecord, 'peekRecord returns the same record'); + assert.equal(generateIdForRecordCalled, 1, 'generateIdForRecord is called once'); + assert.deepEqual(record.serialize(), { + data: { + id: 'manually generated id 1', + type: 'person', + attributes: expectedProps, + }, + }); + }); + + test('store.createRecord does not error if adapter.generateIdForRecord is undefined.', async function (assert) { + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestGenerateIdForRecordAdapter extends EmberObject {} + + this.owner.register('adapter:application', TestGenerateIdForRecordAdapter); + + const props = expectedData.data.attributes; + const record = store.createRecord('person', props); + + assert.deepEqual(record.serialize().data.attributes, props, 'record created without error'); + }); + + test('store.createRecord does not error if adapter is undefined.', async function (assert) { + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + const props = expectedData.data.attributes; + const record = store.createRecord('person', props); + + assert.deepEqual(record.serialize().data.attributes, props, 'record created without error'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/has-many-test.js b/tests/ember-data__adapter/tests/integration/has-many-test.js new file mode 100644 index 00000000000..229503cec81 --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/has-many-test.js @@ -0,0 +1,777 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + const json = { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + relationships: {}, + }, + }; + + snapshot.eachRelationship((key, relationship) => { + if (relationship.kind === 'belongsTo') { + this.serializeBelongsTo(snapshot, json, relationship); + } else if (relationship.kind === 'hasMany') { + this.serializeHasMany(snapshot, json, relationship); + } + }); + + if (Object.keys(json.data.relationships).length === 0) { + delete json.data.relationships; + } + + return json; + } + + // minimal implementation, not json-api compliant + serializeBelongsTo(snapshot, json, relationship) { + const key = relationship.name; + const belongsTo = snapshot.belongsTo(key); + + if (belongsTo) { + const value = { + data: { + id: belongsTo.id, + type: belongsTo.modelName, + }, + }; + json.data.relationships[key] = value; + } + } + + // minimal implementation, not json-api compliant + serializeHasMany(snapshot, json, relationship) { + const key = relationship.name; + const hasMany = snapshot.hasMany(key); + + if (hasMany && hasMany.length) { + const value = { + data: hasMany.map((snap) => ({ + id: snap.id, + type: snap.modelName, + })), + }; + json.data.relationships[key] = value; + } + } +} + +class Post extends Model { + @attr + text; + + @hasMany('comments', { async: true, inverse: 'post' }) + comments; +} + +class Comment extends Model { + @attr + text; + + @belongsTo('post', { async: true, inverse: 'comments' }) + post; +} + +const expectedResult = { + data: [ + { + id: '3', + type: 'comment', + attributes: { + text: 'You rock', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + }, + }, + }, + { + id: '4', + type: 'comment', + attributes: { + text: 'You rock too', + }, + relationships: { + post: { + data: { + id: '2', + type: 'post', + }, + }, + }, + }, + ], +}; + +module('integration/has-many - Has Many Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + }); + + test('if a hasMany relationship has a link but no data (findHasMany is defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let findHasManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + links: { + related: 'https://example.com/api/post/2/comments', + }, + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindHasManyAdapter extends EmberObject { + findRecord() { + findRecordCalled++; + } + + findMany() { + findManyCalled++; + } + + findHasMany(passedStore, snapshot, url, relationship) { + findHasManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findHasMany'); + + const expectedURL = initialRecord.data.relationships.comments.links.related; + assert.equal(url, expectedURL, 'url is passed to findHasMany'); + assert.equal(relationship.name, 'comments', 'relationship is passed to findHasMany'); + + assert.equal(snapshot.modelName, 'post', 'snapshot is passed to findHasMany with correct modelName'); + assert.equal(snapshot.id, '2', 'snapshot is passed to findHasMany with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindHasManyAdapter); + + const post = store.push(initialRecord); + + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.equal(findHasManyCalled, 1, 'findHasMany is called once'); + assert.deepEqual(serializedComments, expectedResult, 'findHasMany returns expected result'); + }); + + testInDebug('if a hasMany relationship has a link but no data (findHasMany is undefined)', async function (assert) { + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + links: { + related: 'https://example.com/api/post/2/comments', + }, + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestFindHasManyAdapter extends EmberObject {} + + owner.register('adapter:application', TestFindHasManyAdapter); + + const post = store.push(initialRecord); + + await assert.expectAssertion(async function () { + await post.comments; + }, /You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'/); + }); + + test('if a hasMany relationship has data but not a link (coalescing is off, findHasMany is defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let findHasManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = false; + + findRecord(passedStore, type, id, snapshot) { + const index = findRecordCalled++; + const expectedId = initialRecord.data.relationships.comments.data[index].id; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Comment, 'model is passed to findRecord'); + assert.equal(id, expectedId, 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve({ data: expectedResultCopy.data[index] }); + } + + findMany() { + findManyCalled++; + } + + findHasMany() { + findHasManyCalled++; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.equal(findHasManyCalled, 0, 'findHasMany is not called'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); + + test('if a hasMany relationship has data but not a link (coalescing is off, findHasMany is not defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = false; + + findRecord(passedStore, type, id, snapshot) { + const index = findRecordCalled++; + const expectedId = initialRecord.data.relationships.comments.data[index].id; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Comment, 'model is passed to findRecord'); + assert.equal(id, expectedId, 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve({ data: expectedResultCopy.data[index] }); + } + + findMany() { + findManyCalled++; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); + + test('if a hasMany relationship has data but not a link (coalescing is on, findHasMany is defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let findHasManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindManyAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + } + + findHasMany() { + findHasManyCalled++; + } + + groupRecordsForFindMany(store, snapshots) { + return [snapshots]; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Comment, 'model is passed to findMany'); + + const expectedIds = expectedResultCopy.data.map((record) => record.id); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindManyAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.equal(findHasManyCalled, 0, 'findHasMany is not called'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); + + test('if a hasMany relationship has data but not a link (coalescing is on, findHasMany is not defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindManyAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + } + + groupRecordsForFindMany(store, snapshots) { + return [snapshots]; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Comment, 'model is passed to findMany'); + + const expectedIds = expectedResultCopy.data.map((record) => record.id); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindManyAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); + + test('if a hasMany relationship has link and data (findHasMany is defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + let findHasManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + links: { + related: 'https://example.com/api/post/2/comments', + }, + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindHasManyAdapter extends EmberObject { + findRecord() { + findRecordCalled++; + } + + findMany() { + findManyCalled++; + } + + findHasMany(passedStore, snapshot, url, relationship) { + findHasManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findHasMany'); + + const expectedURL = initialRecord.data.relationships.comments.links.related; + assert.equal(url, expectedURL, 'url is passed to findHasMany'); + assert.equal(relationship.name, 'comments', 'relationship is passed to findHasMany'); + + assert.equal(snapshot.modelName, 'post', 'snapshot is passed to findHasMany with correct modelName'); + assert.equal(snapshot.id, '2', 'snapshot is passed to findHasMany with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindHasManyAdapter); + + const post = store.push(initialRecord); + + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.equal(findHasManyCalled, 1, 'findHasMany is called once'); + assert.deepEqual(serializedComments, expectedResult, 'findHasMany returns expected result'); + }); + + test('if a hasMany relationship has link and data (coalescing is on, findHasMany is not defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + links: { + related: 'https://example.com/api/post/2/comments', + }, + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindManyAdapter extends EmberObject { + coalesceFindRequests = true; + + findRecord() { + findRecordCalled++; + } + + groupRecordsForFindMany(store, snapshots) { + return [snapshots]; + } + + findMany(passedStore, type, ids, snapshots) { + findManyCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findMany'); + assert.equal(type, Comment, 'model is passed to findMany'); + + const expectedIds = expectedResultCopy.data.map((record) => record.id); + assert.deepEqual(ids, expectedIds, 'ids are passed to findMany'); + + snapshots.forEach((snapshot, index) => { + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findMany with correct modelName'); + assert.equal(snapshot.id, expectedIds[index], 'snapshot is passed to findMany with correct id'); + }); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindManyAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 0, 'findRecord is not called'); + assert.equal(findManyCalled, 1, 'findMany is called once'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); + + test('if a hasMany relationship has link and data (coalescing is off, findHasMany is not defined)', async function (assert) { + let findRecordCalled = 0; + let findManyCalled = 0; + + const initialRecord = { + data: { + id: '2', + type: 'post', + attributes: { + text: "I'm awesome", + }, + relationships: { + comments: { + data: [ + { + id: '3', + type: 'comment', + }, + { + id: '4', + type: 'comment', + }, + ], + }, + }, + }, + }; + + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + coalesceFindRequests = false; + + findRecord(passedStore, type, id, snapshot) { + const index = findRecordCalled++; + const expectedId = initialRecord.data.relationships.comments.data[index].id; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Comment, 'model is passed to findRecord'); + assert.equal(id, expectedId, 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'comment', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, expectedId, 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve({ data: expectedResultCopy.data[index] }); + } + + findMany() { + findManyCalled++; + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const post = store.push(initialRecord); + const comments = await post.comments; + const serializedComments = { + data: comments.slice().map((comment) => comment.serialize().data), + }; + + assert.equal(findRecordCalled, 2, 'findRecord is called twice'); + assert.equal(findManyCalled, 0, 'findMany is not called'); + assert.deepEqual(serializedComments, expectedResult, 'get returns expected result'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/mutations-test.js b/tests/ember-data__adapter/tests/integration/mutations-test.js new file mode 100644 index 00000000000..e830cd761d6 --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/mutations-test.js @@ -0,0 +1,334 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr } from '@ember-data/model'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + return { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + }, + }; + } +} + +class Person extends Model { + @attr + firstName; + + @attr + lastName; +} + +module('integration/mutations - Mutations Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:person', Person); + }); + + test('store.deleteRecord calls adapter.deleteRecord if a record is deleted and then saved', async function (assert) { + let deleteRecordCalled = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestDeleteRecordAdapter extends EmberObject { + deleteRecord(passedStore, type, snapshot) { + deleteRecordCalled++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + } + } + + this.owner.register('adapter:application', TestDeleteRecordAdapter); + + const record = store.push(expectedData); + + record.deleteRecord(); + await record.save(); + + assert.equal(deleteRecordCalled, 1, 'deleteRecord is called once'); + }); + + test('store.deleteRecord calls adapter.deleteRecord if a newly created record is persisted, then deleted and then saved', async function (assert) { + let createRecordCalled = 0; + let deleteRecordCalled = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestDeleteRecordAdapter extends EmberObject { + createRecord(passedStore, type, snapshot) { + createRecordCalled++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + + return Promise.resolve(data); + } + + deleteRecord(passedStore, type, snapshot) { + deleteRecordCalled++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + } + } + + this.owner.register('adapter:application', TestDeleteRecordAdapter); + + const props = { id: expectedData.data.id, ...expectedData.data.attributes }; + const record = store.createRecord('person', props); + await record.save(); + + assert.equal(createRecordCalled, 1, 'createRecord is called once'); + + record.deleteRecord(); + await record.save(); + + assert.equal(deleteRecordCalled, 1, 'deleteRecord is called once'); + }); + + test('store.deleteRecord does not call adapter.deleteRecord if a newly created, unpersisted record is deleted and then saved', async function (assert) { + let createRecordCalled = 0; + let deleteRecordCalled = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestDeleteRecordAdapter extends EmberObject { + createRecord(passedStore, type, snapshot) { + createRecordCalled++; + } + + deleteRecord(passedStore, type, snapshot) { + deleteRecordCalled++; + } + } + + this.owner.register('adapter:application', TestDeleteRecordAdapter); + + const props = { id: expectedData.data.id, ...expectedData.data.attributes }; + const record = store.createRecord('person', props); + + record.deleteRecord(); + await record.save(); + + assert.equal(createRecordCalled, 0, 'adapter.createRecord is not called'); + assert.equal(deleteRecordCalled, 0, 'adapter.deleteRecord is not called'); + }); + + test('record.save() calls adapter.createRecord if a newly created record unpersisted record is saved', async function (assert) { + let createRecordCalled = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestCreateRecordAdapter extends EmberObject { + createRecord(passedStore, type, snapshot) { + createRecordCalled++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + + return Promise.resolve(data); + } + } + + this.owner.register('adapter:application', TestCreateRecordAdapter); + + const props = { id: expectedData.data.id, ...expectedData.data.attributes }; + const record = store.createRecord('person', props); + await record.save(); + + assert.equal(createRecordCalled, 1, 'createRecord is called once'); + }); + + test('record.save() calls adapter.createRecord then adapter.updateRecord if a newly created record record is saved, then saved again', async function (assert) { + let createRecordCalled = 0; + let updateRecord = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestUpdateRecordAdapter extends EmberObject { + createRecord(passedStore, type, snapshot) { + createRecordCalled++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + + return Promise.resolve(data); + } + + updateRecord(passedStore, type, snapshot) { + updateRecord++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + + return Promise.resolve(expectedData); + } + } + + this.owner.register('adapter:application', TestUpdateRecordAdapter); + + const props = { id: expectedData.data.id, ...expectedData.data.attributes }; + const record = store.createRecord('person', props); + await record.save(); + + assert.equal(createRecordCalled, 1, 'createRecord is called once'); + + record.firstName = 'Kevin'; + expectedData.data.attributes.firstName = 'Kevin'; + await record.save(); + + assert.equal(createRecordCalled, 1, 'createRecord is not called again'); + assert.equal(updateRecord, 1, 'updateRecord is called once'); + }); + + test('record.save() calls adapter.updateRecord if an existing persisted record is saved', async function (assert) { + let createRecordCalled = 0; + let updateRecord = 0; + const store = this.owner.lookup('service:store'); + const expectedData = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + class TestUpdateRecordAdapter extends EmberObject { + createRecord(passedStore, type, snapshot) { + createRecordCalled++; + } + + updateRecord(passedStore, type, snapshot) { + updateRecord++; + + const data = snapshot.serialize(); + const id = snapshot.id; + + assert.equal(passedStore, store, 'instance of store is passed to deleteRecord'); + assert.equal(type, Person, 'model is passed to deleteRecord'); + assert.equal(id, '12', 'id is passed to deleteRecord through snapshot'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to deleteRecord with correct modelName'); + assert.deepEqual(data, expectedData, 'snapshot is passed to deleteRecord with correct data'); + + return Promise.resolve(expectedData); + } + } + + this.owner.register('adapter:application', TestUpdateRecordAdapter); + + const record = store.push(expectedData); + + record.firstName = 'Kevin'; + expectedData.data.attributes.firstName = 'Kevin'; + await record.save(); + + assert.equal(createRecordCalled, 0, 'createRecord is not called'); + assert.equal(updateRecord, 1, 'updateRecord is called once'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/queries-test.js b/tests/ember-data__adapter/tests/integration/queries-test.js new file mode 100644 index 00000000000..5f13df715a0 --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/queries-test.js @@ -0,0 +1,278 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr } from '@ember-data/model'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + + serialize(snapshot) { + return { + data: { + id: snapshot.id, + type: snapshot.modelName, + attributes: snapshot.attributes(), + }, + }; + } +} + +class Person extends Model { + @attr + firstName; + + @attr + lastName; +} + +module('integration/queries - Queries Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:person', Person); + }); + + test('options passed to adapters by LegacyHandler are mutable', async function (assert) { + const { owner } = this; + const store = owner.lookup('service:store'); + + const expectedResult = { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }; + + class TestAdapter extends EmberObject { + query(passedStore, type, query, recordArray, adapterOptions) { + assert.deepEqual(query, { initialOption: 'foo' }, 'original query is passed to adapter'); + + query.initialOption = 'bar'; + adapterOptions ||= {}; + adapterOptions.otherOption = 'baz'; + + assert.equal(query.initialOption, 'bar', 'query is mutated'); + assert.equal(adapterOptions.otherOption, 'baz', 'adapterOptions is mutated'); + + return Promise.resolve({ + data: [expectedResult], + }); + } + queryRecord(passedStore, type, query, record, adapterOptions) { + assert.deepEqual(query, { initialOption: 'foo' }, 'original query is passed to adapter'); + + query.initialOption = 'bar'; + adapterOptions ||= {}; + adapterOptions.otherOption = 'baz'; + + assert.equal(query.initialOption, 'bar', 'query is mutated'); + assert.equal(adapterOptions.otherOption, 'baz', 'adapterOptions is mutated'); + + return Promise.resolve({ + data: expectedResult, + }); + } + } + + owner.register('adapter:application', TestAdapter); + + for (const method of ['query', 'queryRecord']) { + const result = await store[method]('person', { initialOption: 'foo' }); + assert.ok(result, `result is returned for ${method}`); + } + }); + + test('store.findRecord calls adapter.findRecord w/correct args', async function (assert) { + let findRecordCalled = 0; + const expectedResult = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + const { owner } = this; + const store = owner.lookup('service:store'); + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + class TestFindRecordAdapter extends EmberObject { + findRecord(passedStore, type, id, snapshot) { + findRecordCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findRecord'); + assert.equal(type, Person, 'model is passed to findRecord'); + assert.equal(id, '12', 'id is passed to findRecord'); + + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findRecord with correct modelName'); + assert.equal(snapshot.id, '12', 'snapshot is passed to findRecord with correct id'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindRecordAdapter); + + const record = await store.findRecord('person', '12'); + + assert.equal(findRecordCalled, 1, 'findRecord is called once'); + assert.deepEqual(record.serialize(), expectedResult, 'findRecord returns expected result'); + }); + + test('store.findAll calls adapter.findAll w/correct args', async function (assert) { + let findAllCalled = 0; + let expectedResult = { + data: [ + { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + ], + }; + + // This code is a workaround for issue https://github.com/emberjs/data/issues/6758 + // expectedResult is mutated during store.findRecord + // to add the lid + const expectedResultCopy = structuredClone(expectedResult); + + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestFindAllAdapter extends EmberObject { + findAll(passedStore, type, sinceToken, snapshot) { + findAllCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to findAll'); + assert.equal(type, Person, 'model is passed to findAll'); + assert.equal(sinceToken, null, 'sinceToken passed to findAll is null'); + assert.equal(snapshot.modelName, 'person', 'snapshot is passed to findAll with correct modelName'); + assert.equal(snapshot.length, 0, 'snapshot is passed to findAll represnts empty array'); + + return Promise.resolve(expectedResultCopy); + } + } + + owner.register('adapter:application', TestFindAllAdapter); + + const manyArray = await store.findAll('person'); + + const result = manyArray.slice().map((person) => person.serialize()); + expectedResult = expectedResult.data.map((person) => ({ data: person })); + + assert.equal(findAllCalled, 1, 'findAll is called once'); + assert.deepEqual(result, expectedResult, 'findAll returns expected result'); + }); + + test('store.queryRecord calls adapter.queryRecord w/correct args', async function (assert) { + let queryRecordCalled = 0; + const expectedResult = { + data: { + id: '12', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestQueryRecordAdapter extends EmberObject { + queryRecord(passedStore, type, query, options) { + queryRecordCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to queryRecord'); + assert.equal(type, Person, 'model is passed to queryRecord'); + assert.deepEqual(query, { firstName: 'Gaurav' }, 'query is passed to queryRecord'); + assert.deepEqual(options, {}, 'options is passsed to queryRecord'); + + return Promise.resolve(expectedResult); + } + } + + owner.register('adapter:application', TestQueryRecordAdapter); + + const record = await store.queryRecord('person', { firstName: 'Gaurav' }); + + assert.equal(queryRecordCalled, 1, 'queryRecord is called once'); + assert.deepEqual(record.serialize(), expectedResult, 'queryRecord returns expected result'); + }); + + test('store.query calls adapter.query w/correct args', async function (assert) { + let queryCalled = 0; + let expectedResult = { + data: [ + { + id: '14', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Tse', + }, + }, + { + id: '19', + type: 'person', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + }, + ], + }; + const { owner } = this; + const store = owner.lookup('service:store'); + + class TestQueryAdapter extends EmberObject { + query(passedStore, type, query, recordArray, options) { + queryCalled++; + + assert.equal(passedStore, store, 'instance of store is passed to query'); + assert.equal(type, Person, 'model is passed to query'); + assert.deepEqual(query, { firstName: 'Chris' }, 'query is passed to query'); + assert.deepEqual(recordArray.slice(), [], 'recordArray is passsed to query'); + assert.deepEqual(options, {}, 'options is passed to query'); + + return Promise.resolve(expectedResult); + } + } + + owner.register('adapter:application', TestQueryAdapter); + + const manyArray = await store.query('person', { firstName: 'Chris' }); + + const result = manyArray.slice().map((person) => person.serialize()); + expectedResult = expectedResult.data.map((person) => ({ data: person })); + + assert.equal(queryCalled, 1, 'query is called once'); + assert.deepEqual(result, expectedResult, 'query returns expected result'); + }); +}); diff --git a/tests/ember-data__adapter/tests/integration/reload-test.js b/tests/ember-data__adapter/tests/integration/reload-test.js new file mode 100644 index 00000000000..db6a85c4327 --- /dev/null +++ b/tests/ember-data__adapter/tests/integration/reload-test.js @@ -0,0 +1,648 @@ +import EmberObject from '@ember/object'; + +import Store from 'ember-data__adapter/services/store'; + +import Model, { attr } from '@ember-data/model'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class MinimalSerializer extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } +} + +class Person extends Model { + @attr + firstName; + + @attr + lastName; +} + +function setupReloadTest(options) { + class TestMinimumAdapter extends EmberObject { + shouldReloadAllCalled = 0; + shouldReloadRecordCalled = 0; + shouldBackgroundReloadAllCalled = 0; + shouldBackgroundReloadRecordCalled = 0; + + requestsMade = 0; + + constructor() { + super(...arguments); + + if (options.shouldReloadAll !== undefined) { + this.shouldReloadAll = function () { + this.shouldReloadAllCalled++; + return options.shouldReloadAll; + }; + } + + if (options.shouldReloadRecord !== undefined) { + this.shouldReloadRecord = function () { + this.shouldReloadRecordCalled++; + return options.shouldReloadRecord; + }; + } + if (options.shouldBackgroundReloadAll !== undefined) { + this.shouldBackgroundReloadAll = function () { + this.shouldBackgroundReloadAllCalled++; + return options.shouldBackgroundReloadAll; + }; + } + + if (options.shouldBackgroundReloadRecord !== undefined) { + this.shouldBackgroundReloadRecord = function () { + this.shouldBackgroundReloadRecordCalled++; + return options.shouldBackgroundReloadRecord; + }; + } + } + + findAll() { + this.requestsMade++; + return Promise.resolve(options.resolveFindAllWith || { data: [] }); + } + + findRecord() { + this.requestsMade++; + return Promise.resolve(options.resolveFindRecordWith || { data: null }); + } + } + this.owner.register('adapter:application', TestMinimumAdapter); + + this.store = this.owner.lookup('service:store'); + this.adapter = this.owner.lookup('adapter:application'); +} + +module('integration/reload - Reloading Tests', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('service:store', Store); + this.owner.register('serializer:application', MinimalSerializer); + this.owner.register('model:person', Person); + }); + + module('adapter.shouldReloadAll', function () { + test('adapter.shouldReloadAll is not called when store.findAll is called with a reload: false flag', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person', { reload: false }); + + assert.equal(this.adapter.shouldReloadAllCalled, 0, 'shouldReloadAll is not called'); + assert.equal(this.adapter.requestsMade, 0, 'no request is made'); + }); + + test('adapter.shouldReloadAll is not called when store.findAll is called with a reload: true flag', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person', { reload: true }); + + assert.equal(this.adapter.shouldReloadAllCalled, 0, 'shouldReloadAll is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('store.findAll does not error if adapter.shouldReloadAll is not defined (records are present)', async function (assert) { + setupReloadTest.call(this, { + shouldBackgroundReloadAll: false, + }); + + this.store.push({ + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('store.findAll does not error if adapter.shouldReloadAll is not defined (records are absent)', async function (assert) { + setupReloadTest.call(this, { + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldReloadAll is called when store.findAll is called without a reload flag (shouldReloadAll is false)', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.shouldReloadAllCalled, 1, 'shouldReloadAll is called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('adapter.shouldReloadAll is called when store.findAll is called without a reload flag (shouldReloadAll is true)', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: true, + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.shouldReloadAllCalled, 1, 'shouldReloadAll is called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + }); + + module('adapter.shouldBackgroundReloadAll', function () { + test('adapter.shouldBackgroundReloadAll is not called called when store.findAll is called with reload: true flag (but we do make request)', async function (assert) { + setupReloadTest.call(this, {}); + + await this.store.findAll('person', { reload: true }); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadAll is not called called when store.findAll is called and adaptershouldReloadAll() returns true (but we do make request)', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: true, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadAll is not called when store.findAll is called with backroundReload: true', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + }); + + await this.store.findAll('person', { backgroundReload: true }); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadAll is not called when store.findAll is called with backroundReload: false', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + }); + + await this.store.findAll('person', { backgroundReload: false }); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 0, 'shouldBackgroundReloadAll is not called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('store.findAll does not error if adapter.shouldBackgroundReloadAll is undefined and backgroundReload is not present.', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadAll is called when store.findAll is called and there is no backgroundReload flag (returns true)', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + shouldBackgroundReloadAll: true, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 1, 'shouldBackgroundReloadAll is called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadAll is called when store.findAll is called and there is no backgroundReload flag (returns false)', async function (assert) { + setupReloadTest.call(this, { + shouldReloadAll: false, + shouldBackgroundReloadAll: false, + }); + + await this.store.findAll('person'); + + assert.equal(this.adapter.shouldBackgroundReloadAllCalled, 1, 'shouldBackgroundReloadAll is called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + }); + + module('adapter.shouldReloadRecord', function () { + test('adapter.shouldReloadRecord is not called when store.findRecord is called for an unloaded record (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + const record = this.store.push(payload); + + this.store.unloadRecord(record); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldReloadRecord is not called when store.findRecord is called for a never loaded record (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldReloadRecord is not called when store.findRecord is called with a reload flag (but we do make request if reload is true)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1', { reload: true }); + + assert.equal(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldReloadRecord is not called when store.findRecord is called with a reload flag (and we do not make request if reload is false)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1', { reload: false }); + + assert.equal(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('if adapter.shouldReloadRecord is undefined, we default to false and do not make a request', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldReloadRecordCalled, 0, 'shouldReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('adapter.shouldReloadRecord is called when store.findRecord is called without a reload flag (shouldReloadRecord returns true)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldReloadRecord: true, + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldReloadRecordCalled, 1, 'shouldReloadRecord is called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldReloadRecord is called when store.findRecord is called without a reload flag (shouldReloadRecord returns false)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldReloadRecord: false, + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldReloadRecordCalled, 1, 'shouldReloadRecord is called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + }); + + module('adapter.shouldBackgroundReloadRecord', function () { + test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called for an unloaded record (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + const record = this.store.push(payload); + + this.store.unloadRecord(record); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called for a never loaded record (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is not called called when store.findRecord is called with reload: true flag (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1', { reload: true }); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is not called called when store.findRecord is called and shouldReloadRecord returns true (but we do make request)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldReloadRecord: true, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called with backroundReload as an option (backgroundReload is true)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1', { backgroundReload: true }); + await this.store._getAllPending(); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is not called when store.findRecord is called with backroundReload as an option (backgroundReload is false)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1', { backgroundReload: false }); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 0, 'shouldBackgroundReloadRecord is not called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + + test('store.findRecord does not error if adapter.shouldBackgroundReloadRecord is undefined and backgroundReload is not present.', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + await this.store._getAllPending(); + + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is called when store.findRecord is called and there is no backgroundReload flag (adapter.shouldBackgroundReloadRecord() returns true)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: true, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + await this.store._getAllPending(); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 1, 'shouldBackgroundReloadRecord is called'); + assert.equal(this.adapter.requestsMade, 1, 'an ajax request is made'); + }); + + test('adapter.shouldBackgroundReloadRecord is called when store.findRecord is called and there is no backgroundReload flag (adapter.shouldBackgroundReloadRecord() returns false)', async function (assert) { + const payload = { + data: { + id: '1', + type: 'person', + attributes: { + firstName: 'Gaurav', + lastName: 'Munjal', + }, + }, + }; + + setupReloadTest.call(this, { + shouldBackgroundReloadRecord: false, + resolveFindRecordWith: payload, + }); + + this.store.push(payload); + + await this.store.findRecord('person', '1'); + + assert.equal(this.adapter.shouldBackgroundReloadRecordCalled, 1, 'shouldBackgroundReloadRecord is called'); + assert.equal(this.adapter.requestsMade, 0, 'no ajax request is made'); + }); + }); +}); diff --git a/tests/ember-data__adapter/tests/test-helper.js b/tests/ember-data__adapter/tests/test-helper.js new file mode 100644 index 00000000000..8d90206e905 --- /dev/null +++ b/tests/ember-data__adapter/tests/test-helper.js @@ -0,0 +1,24 @@ +import { setApplication } from '@ember/test-helpers'; + +import configureAsserts from '@ember-data/unpublished-test-infra/test-support/asserts/index'; +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; + +import Application from '../app'; +import config from '../config/environment'; + +setupGlobalHooks((hooks) => { + configureAsserts(hooks); +}); + +configure(); + +setApplication(Application.create(config.APP)); +start({ + tryCatch: false, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/ember-data__adapter/tsconfig.json b/tests/ember-data__adapter/tsconfig.json new file mode 100644 index 00000000000..527a59ee167 --- /dev/null +++ b/tests/ember-data__adapter/tsconfig.json @@ -0,0 +1,99 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../../packages/serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../../packages/serializer/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], + "@warp-drive/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/serializer" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/diagnostic" + } + ] +} diff --git a/tests/ember-data__graph/README.md b/tests/ember-data__graph/README.md new file mode 100644 index 00000000000..82b06f0ca94 --- /dev/null +++ b/tests/ember-data__graph/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/graph + +This test-package provides tests for the `@ember-data/graph` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__graph; +pnpm test; +``` diff --git a/tests/ember-data__graph/app/app.ts b/tests/ember-data__graph/app/app.ts new file mode 100644 index 00000000000..8f235d166e6 --- /dev/null +++ b/tests/ember-data__graph/app/app.ts @@ -0,0 +1,16 @@ +import Application from '@ember/application'; + +import loadInitializers from 'ember-load-initializers'; + +import config from './config/environment'; +import Resolver from './resolver'; + +class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + override Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/tests/ember-data__graph/app/config/environment.d.ts b/tests/ember-data__graph/app/config/environment.d.ts new file mode 100644 index 00000000000..5d685ce5f0a --- /dev/null +++ b/tests/ember-data__graph/app/config/environment.d.ts @@ -0,0 +1,17 @@ +export default config; + +/** + * Type declarations for + * import config from './config/environment' + * + * For now these need to be managed by the developer + * since different ember addons can materialize new entries. + */ +declare const config: { + environment: 'production' | 'development' | 'testing'; + modulePrefix: string; + podModulePrefix: string; + locationType: string; + rootURL: string; + APP: object; +}; diff --git a/tests/ember-data__graph/app/index.html b/tests/ember-data__graph/app/index.html new file mode 100644 index 00000000000..99697296d7b --- /dev/null +++ b/tests/ember-data__graph/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/graph Tests + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/json-api/app/resolver.ts b/tests/ember-data__graph/app/resolver.ts similarity index 100% rename from tests/json-api/app/resolver.ts rename to tests/ember-data__graph/app/resolver.ts diff --git a/tests/json-api/app/router.ts b/tests/ember-data__graph/app/router.ts similarity index 100% rename from tests/json-api/app/router.ts rename to tests/ember-data__graph/app/router.ts diff --git a/tests/ember-data__graph/app/services/store.ts b/tests/ember-data__graph/app/services/store.ts new file mode 100644 index 00000000000..2e360e8abb2 --- /dev/null +++ b/tests/ember-data__graph/app/services/store.ts @@ -0,0 +1,60 @@ +import JSONAPICache from '@ember-data/json-api'; +import { + adapterFor, + cleanup, + LegacyNetworkHandler, + normalize, + pushPayload, + serializeRecord, + serializerFor, +} from '@ember-data/legacy-compat'; +import type Model from '@ember-data/model'; +import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember-data/model/hooks'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import BaseStore, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; + +export default class Store extends BaseStore { + constructor(args: unknown) { + super(args); + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } + + createSchemaService(): ReturnType { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager) { + return new JSONAPICache(capabilities); + } + + override instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: Record): Model { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model) { + teardownRecord.call(this, record); + } + + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { + return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type); + } + + serializeRecord = serializeRecord; + pushPayload = pushPayload; + adapterFor = adapterFor; + serializerFor = serializerFor; + normalize = normalize; + + override destroy() { + cleanup.call(this); + super.destroy(); + } +} diff --git a/tests/graph/app/styles/app.css b/tests/ember-data__graph/app/styles/app.css similarity index 100% rename from tests/graph/app/styles/app.css rename to tests/ember-data__graph/app/styles/app.css diff --git a/tests/adapter-encapsulation/app/models/.gitkeep b/tests/ember-data__graph/app/templates/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/models/.gitkeep rename to tests/ember-data__graph/app/templates/.gitkeep diff --git a/tests/graph/app/templates/application.hbs b/tests/ember-data__graph/app/templates/application.hbs similarity index 100% rename from tests/graph/app/templates/application.hbs rename to tests/ember-data__graph/app/templates/application.hbs diff --git a/tests/ember-data__graph/config/environment.js b/tests/ember-data__graph/config/environment.js new file mode 100644 index 00000000000..94e67a34e77 --- /dev/null +++ b/tests/ember-data__graph/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__graph', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/debug-encapsulation/config/optional-features.json b/tests/ember-data__graph/config/optional-features.json similarity index 100% rename from tests/debug-encapsulation/config/optional-features.json rename to tests/ember-data__graph/config/optional-features.json diff --git a/tests/debug-encapsulation/config/targets.js b/tests/ember-data__graph/config/targets.js similarity index 100% rename from tests/debug-encapsulation/config/targets.js rename to tests/ember-data__graph/config/targets.js diff --git a/tests/ember-data__graph/diagnostic.js b/tests/ember-data__graph/diagnostic.js new file mode 100644 index 00000000000..ede75dbb1ad --- /dev/null +++ b/tests/ember-data__graph/diagnostic.js @@ -0,0 +1,3 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; + +await launch(); diff --git a/tests/ember-data__graph/ember-cli-build.js b/tests/ember-data__graph/ember-cli-build.js new file mode 100644 index 00000000000..837d9d3e6d3 --- /dev/null +++ b/tests/ember-data__graph/ember-cli-build.js @@ -0,0 +1,31 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + }); + + 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'); + + return app.toTree(); +}; diff --git a/tests/ember-data__graph/eslint.config.mjs b/tests/ember-data__graph/eslint.config.mjs new file mode 100644 index 00000000000..9b1fba199bb --- /dev/null +++ b/tests/ember-data__graph/eslint.config.mjs @@ -0,0 +1,26 @@ +// @ts-check +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 * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + diagnostic.browser(), +]; diff --git a/tests/ember-data__graph/package.json b/tests/ember-data__graph/package.json new file mode 100644 index 00000000000..cd90cd2169a --- /dev/null +++ b/tests/ember-data__graph/package.json @@ -0,0 +1,124 @@ +{ + "name": "ember-data__graph", + "version": "4.12.8", + "private": true, + "description": "Provides tests for @ember-data/graph", + "keywords": [], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "tests/ember-data__graph" + }, + "license": "MIT", + "author": "", + "directories": { + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "test": "bun ./diagnostic.js", + "test:production": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@warp-drive/diagnostic": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", + "@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.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-test-loader": "^3.1.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "ember-source-channel-url": "^3.0.0", + "ember-try": "^3.0.0", + "loader.js": "^4.7.0", + "silent-error": "^1.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "ember": { + "edition": "octane" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } +} diff --git a/tests/ember-data__graph/tests/index.html b/tests/ember-data__graph/tests/index.html new file mode 100644 index 00000000000..e63dd28e94e --- /dev/null +++ b/tests/ember-data__graph/tests/index.html @@ -0,0 +1,37 @@ + + + + + + @ember-data/graph Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+ + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + + diff --git a/tests/ember-data__graph/tests/integration/graph.ts b/tests/ember-data__graph/tests/integration/graph.ts new file mode 100644 index 00000000000..526d87cf670 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph.ts @@ -0,0 +1,7 @@ +import { module, test } from '@warp-drive/diagnostic'; + +module('RecordData', function () { + test('Test Suit Configured', function (assert) { + assert.ok('We are configured'); + }); +}); 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 new file mode 100644 index 00000000000..c83a198176d --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/diff-preservation-test.ts @@ -0,0 +1,1329 @@ +// Remove once @hasMany is typed +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +import { deprecatedTest } from '../../setup-test'; + +module('Integration | Graph | Diff Preservation', function (hooks) { + setupTest(hooks); + + deprecatedTest( + 'updateRelationship operation filters duplicates', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0.0', + count: 1, + }, + function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'configs', + record: appIdentifier, + value: { + data: [ + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '2' }, + { type: 'config', id: '3' }, + { type: 'config', id: '4' }, + ], + }, + }); + }); + + const data = graph.getData(appIdentifier, 'configs'); + assert.deepEqual( + JSON.parse(JSON.stringify(data)), + { + data: [ + { type: 'config', id: '1', lid: '@lid:config-1' }, + { type: 'config', id: '2', lid: '@lid:config-2' }, + { type: 'config', id: '3', lid: '@lid:config-3' }, + { type: 'config', id: '4', lid: '@lid:config-4' }, + ], + }, + 'we have the expected data' + ); + } + ); + + deprecatedTest( + 'replaceRelatedRecords operation filters duplicates in a local replace', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0.0', + count: 1, + }, + function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + const configIdentifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + const configIdentifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '2' }); + const configIdentifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '3' }); + const configIdentifier4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [ + configIdentifier1, + configIdentifier1, + configIdentifier1, + configIdentifier2, + configIdentifier3, + configIdentifier4, + ], + }); + }); + + const data = graph.getData(appIdentifier, 'configs'); + assert.deepEqual( + JSON.parse(JSON.stringify(data)), + { + data: [ + { type: 'config', id: '1', lid: '@lid:config-1' }, + { type: 'config', id: '2', lid: '@lid:config-2' }, + { type: 'config', id: '3', lid: '@lid:config-3' }, + { type: 'config', id: '4', lid: '@lid:config-4' }, + ], + }, + 'we have the expected data' + ); + } + ); + + if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + test('updateRelationship operation from the collection side does not clear local state', function (assert) { + // tests that Many:Many, Many:One do not clear local state from + // either side when updating the relationship from the Many 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 namespaces + 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 each relationship + // we change the app for each config to either null or a different app + // we remove each namespace from the app + store._join(() => { + ['1', '2', '3', '4'].forEach((id) => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', id), + value: id === '1' || id === '2' ? null : identifier('app', '2'), + }); + }); + graph.update({ + op: 'replaceRelatedRecords', + field: 'namespaces', + record: appIdentifier, + value: [], + }); + }); + + // assert app relationships + let configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + let namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // updateRelationship from the collection side + // this should not clear the local state + // so the configs should still be empty or have the new app + // and the namespaces should still have the app removed + store._join(() => { + 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' }, + ], + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // Commit the dirty state + store._join(() => { + ['1', '2', '3', '4'].forEach((id) => { + let record = identifier('config', id); + graph.push({ + op: 'updateRelationship', + field: 'app', + record, + value: graph.getData(record, 'app'), + }); + + record = identifier('namespace', id); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record, + value: graph.getData(record, 'apps'), + }); + }); + }); + + // Ensure our state is still the same + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // push a new state from the server + // there should be no local state left, so this should result + // in the observable state matching the new remote state + // however the order of the namespaces should now be different + store._join(() => { + 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' }, + ], + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configRelationship.data, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3'), identifier('config', '4')], + 'configs are correct' + ); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals( + namespaceRelationship.data, + [ + identifier('namespace', '1'), + identifier('namespace', '2'), + identifier('namespace', '3'), + identifier('namespace', '4'), + ], + 'namespaces are correct' + ); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual(appRelationship.data, identifier('app', '1'), `config ${id} app relationship is correct`); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3'), identifier('app', '1')], + `namespace ${id} apps relationship is correct` + ); + }); + }); + + test('updateRelationship operation from the belongsTo side does not clear local state', function (assert) { + // tests that One:Many, One:One do not clear local state from + // either side when updating the relationship from the One side + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @belongsTo('config', { async: false, inverse: 'app' }) declare config: Config[]; + @belongsTo('namespace', { async: false, inverse: 'apps' }) declare namespace: Namespace | null; + @belongsTo('cluster', { async: false, inverse: 'app' }) declare cluster: Cluster | null; + } + class Cluster extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'cluster' }) declare app: App | null; + } + + class Namespace extends Model { + @attr declare name: string; + @hasMany('app', { async: false, inverse: 'namespace' }) declare apps: App[]; + } + + class Config extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'config' }) 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); + owner.register('model:cluster', Cluster); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = identifier('app', '1'); + const configIdentifier = identifier('config', '1'); + const clusterIdentifier = identifier('cluster', '1'); + const namespaceIdentifier = identifier('namespace', '1'); + + // set initial state + // one app, with 1 config, 1 cluster and 1 namespace + // the config belongs to the app + // the cluster belongs to the app + // the 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: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: identifier('namespace', '1'), + value: { + data: [ + { type: 'app', id: '1' }, + { type: 'app', id: '2' }, + { type: 'app', id: '3' }, + ], + }, + }); + }); + + // mutate each relationship + // we change the app for the config null + // we change the app for the cluster to a different app + // we remove the app from the namespace + store._join(() => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', '1'), + value: null, + }); + graph.update({ + op: 'removeFromRelatedRecords', + field: 'apps', + record: identifier('namespace', '1'), + value: appIdentifier, + }); + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('cluster', '1'), + value: identifier('app', '3'), + }); + }); + + // assert app relationships + let configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + let clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + let namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + let appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + let clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + let namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // update the belongsTo side + // this should not clear the local state + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // Commit the dirty state + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'app', + record: configIdentifier, + value: graph.getData(configIdentifier, 'app'), + }); + graph.push({ + op: 'updateRelationship', + field: 'app', + record: clusterIdentifier, + value: graph.getData(clusterIdentifier, 'app'), + }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: namespaceIdentifier, + value: graph.getData(namespaceIdentifier, 'apps'), + }); + }); + + // Ensure our state is still the same + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // push a new state from the server + // there should be no local state left, so this should result + // in the observable state matching the new remote state + // however the order of the namespaces should now be different + // since we removed the app from the namespace + // and then readd it + // without receiving a new ordering for the array from the API + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, configIdentifier, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, clusterIdentifier, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, namespaceIdentifier, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, appIdentifier, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, appIdentifier, 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3'), appIdentifier], + 'namespace apps relationship is correct' + ); + }); + } + + test('updateRelationship operation from the collection side does not clear local state when `resetOnRemoteUpdate: false` is set', 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', resetOnRemoteUpdate: false }) declare apps: App[]; + } + + class Config extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'configs', resetOnRemoteUpdate: false }) 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 namespaces + 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 each relationship + // we change the app for each config to either null or a different app + // we remove each namespace from the app + store._join(() => { + ['1', '2', '3', '4'].forEach((id) => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', id), + value: id === '1' || id === '2' ? null : identifier('app', '2'), + }); + }); + graph.update({ + op: 'replaceRelatedRecords', + field: 'namespaces', + record: appIdentifier, + value: [], + }); + }); + + // assert app relationships + let configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + let namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // updateRelationship from the collection side + // this should not clear the local state + // so the configs should still be empty or have the new app + // and the namespaces should still have the app removed + store._join(() => { + 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' }, + ], + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // Commit the dirty state + store._join(() => { + ['1', '2', '3', '4'].forEach((id) => { + let record = identifier('config', id); + graph.push({ + op: 'updateRelationship', + field: 'app', + record, + value: graph.getData(record, 'app'), + }); + + record = identifier('namespace', id); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record, + value: graph.getData(record, 'apps'), + }); + }); + }); + + // Ensure our state is still the same + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals(configRelationship.data, [], 'configs are correct'); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals(namespaceRelationship.data, [], 'namespaces are correct'); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual( + appRelationship.data, + id === '1' || id === '2' ? null : identifier('app', '2'), + `config ${id} app relationship is correct` + ); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + `namespace ${id} apps relationship is correct` + ); + }); + + // push a new state from the server + // there should be no local state left, so this should result + // in the observable state matching the new remote state + // however the order of the namespaces should now be different + store._join(() => { + 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' }, + ], + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configRelationship.data, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3'), identifier('config', '4')], + 'configs are correct' + ); + + namespaceRelationship = graph.getData(appIdentifier, 'namespaces'); + assert.arrayStrictEquals( + namespaceRelationship.data, + [ + identifier('namespace', '1'), + identifier('namespace', '2'), + identifier('namespace', '3'), + identifier('namespace', '4'), + ], + 'namespaces are correct' + ); + + // assert config relationships + ['1', '2', '3', '4'].forEach((id) => { + const configIdentifier = identifier('config', id); + const appRelationship = graph.getData(configIdentifier, 'app'); + assert.deepEqual(appRelationship.data, identifier('app', '1'), `config ${id} app relationship is correct`); + }); + + // assert namespace relationships + ['1', '2', '3', '4'].forEach((id) => { + const namespaceIdentifier = identifier('namespace', id); + const appRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + appRelationship.data, + [identifier('app', '2'), identifier('app', '3'), identifier('app', '1')], + `namespace ${id} apps relationship is correct` + ); + }); + }); + + test('updateRelationship operation from the belongsTo side does not clear local state when `resetOnRemoteUpdate: false` is set', function (assert) { + // tests that One:Many, One:One do not clear local state from + // either side when updating the relationship from the One 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; + @belongsTo('config', { async: false, inverse: 'app' }) declare config: Config[]; + @belongsTo('namespace', { async: false, inverse: 'apps' }) declare namespace: Namespace | null; + @belongsTo('cluster', { async: false, inverse: 'app' }) declare cluster: Cluster | null; + } + class Cluster extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'cluster', resetOnRemoteUpdate: false }) declare app: App | null; + } + + class Namespace extends Model { + @attr declare name: string; + @hasMany('app', { async: false, inverse: 'namespace', resetOnRemoteUpdate: false }) declare apps: App[]; + } + + class Config extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'config', resetOnRemoteUpdate: false }) 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); + owner.register('model:cluster', Cluster); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = identifier('app', '1'); + const configIdentifier = identifier('config', '1'); + const clusterIdentifier = identifier('cluster', '1'); + const namespaceIdentifier = identifier('namespace', '1'); + + // set initial state + // one app, with 1 config, 1 cluster and 1 namespace + // the config belongs to the app + // the cluster belongs to the app + // the 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: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: identifier('namespace', '1'), + value: { + data: [ + { type: 'app', id: '1' }, + { type: 'app', id: '2' }, + { type: 'app', id: '3' }, + ], + }, + }); + }); + + // mutate each relationship + // we change the app for the config null + // we change the app for the cluster to a different app + // we remove the app from the namespace + store._join(() => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', '1'), + value: null, + }); + graph.update({ + op: 'removeFromRelatedRecords', + field: 'apps', + record: identifier('namespace', '1'), + value: appIdentifier, + }); + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('cluster', '1'), + value: identifier('app', '3'), + }); + }); + + // assert app relationships + let configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + let clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + let namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + let appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + let clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + let namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // update the belongsTo side + // this should not clear the local state + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // Commit the dirty state + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'app', + record: configIdentifier, + value: graph.getData(configIdentifier, 'app'), + }); + graph.push({ + op: 'updateRelationship', + field: 'app', + record: clusterIdentifier, + value: graph.getData(clusterIdentifier, 'app'), + }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: namespaceIdentifier, + value: graph.getData(namespaceIdentifier, 'apps'), + }); + }); + + // Ensure our state is still the same + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, null, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, null, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, null, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, null, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, identifier('app', '3'), 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3')], + 'namespace apps relationship is correct' + ); + + // push a new state from the server + // there should be no local state left, so this should result + // in the observable state matching the new remote state + // however the order of the namespaces should now be different + // since we removed the app from the namespace + // and then readd it + // without receiving a new ordering for the array from the API + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'config', + record: appIdentifier, + value: { + data: { type: 'config', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'namespace', + record: appIdentifier, + value: { + data: { type: 'namespace', id: '1' }, + }, + }); + }); + + // assert app relationships + configRelationship = graph.getData(appIdentifier, 'config'); + assert.equal(configRelationship.data, configIdentifier, 'config is correct'); + clusterRelationship = graph.getData(appIdentifier, 'cluster'); + assert.deepEqual(clusterRelationship.data, clusterIdentifier, 'cluster is correct'); + namespaceRelationship = graph.getData(appIdentifier, 'namespace'); + assert.deepEqual(namespaceRelationship.data, namespaceIdentifier, 'namespace is correct'); + + // assert config relationships + appRelationship = graph.getData(configIdentifier, 'app'); + assert.equal(appRelationship.data, appIdentifier, 'config app relationship is correct'); + clusterAppRelationship = graph.getData(clusterIdentifier, 'app'); + assert.deepEqual(clusterAppRelationship.data, appIdentifier, 'cluster app relationship is correct'); + namespaceAppsRelationship = graph.getData(namespaceIdentifier, 'apps'); + assert.arrayStrictEquals( + namespaceAppsRelationship.data, + [identifier('app', '2'), identifier('app', '3'), appIdentifier], + 'namespace apps relationship is correct' + ); + }); +}); diff --git a/tests/ember-data__graph/tests/integration/graph/duplicate-data-test.ts b/tests/ember-data__graph/tests/integration/graph/duplicate-data-test.ts new file mode 100644 index 00000000000..cbe21bbe895 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/duplicate-data-test.ts @@ -0,0 +1,231 @@ +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { DEPRECATE_NON_UNIQUE_PAYLOADS } from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +import { deprecatedTest } from '../../setup-test'; + +module('Integration | Graph | Duplicate Data', function (hooks) { + setupTest(hooks); + + deprecatedTest( + 'updateRelationship operation filters duplicates', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0.0', + count: 1, + }, + function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'configs', + record: appIdentifier, + value: { + data: [ + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '2' }, + { type: 'config', id: '3' }, + { type: 'config', id: '4' }, + ], + }, + }); + }); + + const data = graph.getData(appIdentifier, 'configs'); + assert.deepEqual( + JSON.parse(JSON.stringify(data)), + { + data: [ + { type: 'config', id: '1', lid: '@lid:config-1' }, + { type: 'config', id: '2', lid: '@lid:config-2' }, + { type: 'config', id: '3', lid: '@lid:config-3' }, + { type: 'config', id: '4', lid: '@lid:config-4' }, + ], + }, + 'we have the expected data' + ); + } + ); + + deprecatedTest( + 'replaceRelatedRecords operation filters duplicates in a local replace', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0.0', + count: 1, + }, + function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + const configIdentifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + const configIdentifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '2' }); + const configIdentifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '3' }); + const configIdentifier4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [ + configIdentifier1, + configIdentifier1, + configIdentifier1, + configIdentifier2, + configIdentifier3, + configIdentifier4, + ], + }); + }); + + const data = graph.getData(appIdentifier, 'configs'); + assert.deepEqual( + JSON.parse(JSON.stringify(data)), + { + data: [ + { type: 'config', id: '1', lid: '@lid:config-1' }, + { type: 'config', id: '2', lid: '@lid:config-2' }, + { type: 'config', id: '3', lid: '@lid:config-3' }, + { type: 'config', id: '4', lid: '@lid:config-4' }, + ], + }, + 'we have the expected data' + ); + } + ); + + if (!DEPRECATE_NON_UNIQUE_PAYLOADS) { + if (DEBUG) { + test('updateRelationship operation asserts on duplicates in remote payloads', function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + + try { + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'configs', + record: appIdentifier, + value: { + data: [ + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '1' }, + { type: 'config', id: '2' }, + { type: 'config', id: '3' }, + { type: 'config', id: '4' }, + ], + }, + }); + }); + assert.ok(false, 'expected assertion'); + } catch (e: unknown) { + assert.equal( + (e as Error)?.message, + 'Expected all entries in the relationship to be unique, found duplicates', + 'assertion is thrown' + ); + } + }); + + test('replaceRelatedRecords asserts on duplicates in a local replace', function (assert) { + const { owner } = this; + + class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; + } + + class Config extends Model { + @attr declare name: string; + } + + owner.register('model:app', App); + owner.register('model:config', Config); + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + const configIdentifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + const configIdentifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '2' }); + const configIdentifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '3' }); + const configIdentifier4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + + try { + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [ + configIdentifier1, + configIdentifier1, + configIdentifier1, + configIdentifier2, + configIdentifier3, + configIdentifier4, + ], + }); + }); + assert.ok(false, 'expected assertion'); + } catch (e: unknown) { + assert.equal( + (e as Error)?.message, + 'Expected all entries in the relationship to be unique, found duplicates', + 'assertion is thrown' + ); + } + }); + } + } +}); diff --git a/tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts b/tests/ember-data__graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts similarity index 76% rename from tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts rename to tests/ember-data__graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts index c23354f52e6..5c15a0ec175 100644 --- a/tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts @@ -1,32 +1,10 @@ -import { settled } from '@ember/test-helpers'; - -import { module, test as runTest } from 'qunit'; +import { module, test } from '@warp-drive/diagnostic'; import type { TestConfig } from './helpers'; import { setInitialState, testFinalState } from './helpers'; import type { Context } from './setup'; import { setupGraphTest } from './setup'; -/** - * qunit-console-grouper groups by test but includes setup/teardown - * and in-test as all one block. Adding this grouping allows us to - * clearly notice when a log came during the test vs during setup/teardown. - * - * We should upstream this behavior to qunit-console-grouper - */ -async function test(name: string, callback) { - const fn = async function (this: Context, ...args) { - console.groupCollapsed(name); // eslint-disable-line no-console - try { - await callback.call(this, ...args); - } finally { - console.groupEnd(); // eslint-disable-line no-console - console.log(`====(Begin Test Teardown)====`); // eslint-disable-line no-console - } - }; - return runTest(name, fn); -} - module('Integration | Graph | Edge Removal', function (hooks) { setupGraphTest(hooks); @@ -103,8 +81,8 @@ module('Integration | Graph | Edge Removal', function (hooks) { // now we delete john.deleteRecord(); - // just in case there is a backburner flush - await settled(); + // just in case there is async work + await Promise.resolve(); /** * For deletions, since no state change has been persisted, we expect the cache to still @@ -116,7 +94,7 @@ module('Integration | Graph | Edge Removal', function (hooks) { * However: for a newly created record any form of rollback, unload or persisted delete * will result in it being destroyed and cleared */ - await testFinalState( + testFinalState( this, testState, config, @@ -147,8 +125,8 @@ module('Integration | Graph | Edge Removal', function (hooks) { // now we unload john.unloadRecord(); - // just in case there is a backburner flush - await settled(); + // just in case there is async work + await Promise.resolve(); /** * For unload, we treat it as a persisted deletion for new records and for sync relationships and @@ -174,11 +152,11 @@ module('Integration | Graph | Edge Removal', function (hooks) { */ // we remove if the record was new or if the relationship was sync (client side delete semantics) - let removed = config.useCreate || !config.async; - // we clear sync non-implicit relationships (client side delete semantics) - let cleared = !config.async && !config.inverseNull; + const removed = config.useCreate || !config.async; + // we clear new records, or sync non-implicit relationships (client side delete semantics) + const cleared = config.useCreate || (!config.async && !config.inverseNull); - await testFinalState(this, testState, config, { removed, cleared, implicitCleared: true }, assert); + testFinalState(this, testState, config, { removed, cleared, implicitCleared: true }, assert); }); } @@ -193,7 +171,7 @@ module('Integration | Graph | Edge Removal', function (hooks) { }); }); - module('Persisted Deletion w/o dematerialization of Record removes it from the graph', function (hooks) { + module('Persisted Deletion w/o dematerialization of Record removes it from the graph', function (innerHooks) { function persistedDeletionTest(config: TestConfig) { test(config.name, async function (this: Context, assert) { const testState = await setInitialState(this, config, assert); @@ -224,7 +202,7 @@ module('Integration | Graph | Edge Removal', function (hooks) { if (config.relType === 'hasMany' && !config.async && config.dirtyLocal) { cleared = false; } - await testFinalState(this, testState, config, { removed: true, cleared, implicitCleared: true }, assert); + testFinalState(this, testState, config, { removed: true, cleared, implicitCleared: true }, assert); }); } @@ -239,31 +217,35 @@ module('Integration | Graph | Edge Removal', function (hooks) { }); }); - module('Persisted Deletion + dematerialization of Record removes it from the graph and cleans up', function (hooks) { - function persistedDeletionUnloadedTest(config: TestConfig) { - test(config.name, async function (this: Context, assert) { - const testState = await setInitialState(this, config, assert); - const { john } = testState; - - // now we delete - john.deleteRecord(); - await john.save(); - john.unloadRecord(); - - await settled(); - - await testFinalState(this, testState, config, { removed: true, cleared: true }, assert); + module( + 'Persisted Deletion + dematerialization of Record removes it from the graph and cleans up', + function (innerHooks) { + function persistedDeletionUnloadedTest(config: TestConfig) { + test(config.name, async function (this: Context, assert) { + const testState = await setInitialState(this, config, assert); + const { john } = testState; + + // now we delete + john.deleteRecord(); + await john.save(); + john.unloadRecord(); + + // just in case there is async work + await Promise.resolve(); + + testFinalState(this, testState, config, { removed: true, cleared: true }, assert); + }); + } + + TestScenarios.forEach(persistedDeletionUnloadedTest); + TestScenarios.forEach((testConfig) => { + const config = Object.assign({}, testConfig, { name: `[Newly Created] ${testConfig.name}`, useCreate: true }); + persistedDeletionUnloadedTest(config); + }); + TestScenarios.forEach((testConfig) => { + const config = Object.assign({}, testConfig, { name: `[LOCAL STATE] ${testConfig.name}`, dirtyLocal: true }); + persistedDeletionUnloadedTest(config); }); } - - TestScenarios.forEach(persistedDeletionUnloadedTest); - TestScenarios.forEach((testConfig) => { - const config = Object.assign({}, testConfig, { name: `[Newly Created] ${testConfig.name}`, useCreate: true }); - persistedDeletionUnloadedTest(config); - }); - TestScenarios.forEach((testConfig) => { - const config = Object.assign({}, testConfig, { name: `[LOCAL STATE] ${testConfig.name}`, dirtyLocal: true }); - persistedDeletionUnloadedTest(config); - }); - }); + ); }); diff --git a/tests/graph/tests/integration/graph/edge-removal/helpers.ts b/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts similarity index 76% rename from tests/graph/tests/integration/graph/edge-removal/helpers.ts rename to tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts index 456eee90498..305e644a8c5 100644 --- a/tests/graph/tests/integration/graph/edge-removal/helpers.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts @@ -1,9 +1,9 @@ -import { settled } from '@ember/test-helpers'; - -import { ImplicitRelationship } from '@ember-data/graph/-private/graph'; +// Remove this disable once @belongsTo is typed import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import type { Diagnostic } from '@warp-drive/diagnostic/-types'; import type { Context, UserRecord } from './setup'; import { stateOf } from './setup'; @@ -73,7 +73,23 @@ interface TestState { johnInverseKey: string; } -export async function setInitialState(context: Context, config: TestConfig, assert): Promise { +type UserRef = { type: 'user'; id: string }; +type BestFriendRel = { + bestFriends: { + data: T; + }; +}; + +function makeRel(id: string | null, isMany: false): BestFriendRel; +function makeRel(id: string | null, isMany: true): BestFriendRel; +function makeRel(id: string | null, isMany: boolean): BestFriendRel { + const ref = { type: 'user', id: id as string } as const; + const data = isMany ? (id === null ? [] : [ref]) : id === null ? null : ref; + + return { bestFriends: { data } }; +} + +export async function setInitialState(context: Context, config: TestConfig, assert: Diagnostic): Promise { const { owner, store, graph } = context; const { identifierCache } = store; const isMany = config.relType === 'hasMany'; @@ -85,79 +101,78 @@ export async function setInitialState(context: Context, config: TestConfig, asse }; class User extends Model { - @attr name; - @relFn('user', relConfig) bestFriends; + @attr declare name: string; + @relFn('user', relConfig) declare bestFriends: unknown; } owner.register('model:user', User); - function makeRel(id: string | null): any { - let ref = { type: 'user', id }; - const data = isMany ? (id === null ? [] : [ref]) : id === null ? null : ref; - - return { bestFriends: { data } }; - } - - let chris, john, johnIdentifier; + let chris: UserRecord, john: UserRecord, johnIdentifier: StableRecordIdentifier; if (!config.useCreate) { - const data = { + const data: CollectionResourceDocument<'user'> = { data: [ { type: 'user', id: '1', attributes: { name: 'Chris' }, - relationships: makeRel(config.dirtyLocal ? null : '2'), + relationships: makeRel(config.dirtyLocal ? null : '2', isMany as true), }, { type: 'user', id: '2', attributes: { name: 'John' }, - relationships: makeRel(config.dirtyLocal ? null : '1'), + relationships: makeRel(config.dirtyLocal ? null : '1', isMany as true), }, ], }; - [chris, john] = store.push(data); + [chris, john] = store.push(data); johnIdentifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); } else { - chris = store.push({ + chris = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' }, }, }); - john = store.createRecord('user', { name: 'John', bestFriends: isMany ? [chris] : chris }); + // @ts-expect-error + john = store.createRecord('user', { name: 'John', bestFriends: isMany ? [chris] : chris }); johnIdentifier = recordIdentifierFor(john); } if (config.dirtyLocal) { if (isMany) { - let friends = await john.bestFriends; + let friends: UserRecord[] = await (john.bestFriends as unknown as Promise); friends.push(chris); if (config.inverseNull) { - friends = await chris.bestFriends; + friends = await (chris.bestFriends as unknown as Promise); friends.push(john); } } else { + // @ts-expect-error john.bestFriends = chris; + // @ts-expect-error chris.bestFriends = john; } } - await settled(); + // give ourselves a tick in case there was async work + await Promise.resolve(); const chrisIdentifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const chrisBestFriend = graph.get(chrisIdentifier, 'bestFriends'); const johnBestFriend = graph.get(johnIdentifier, 'bestFriends'); // pre-conds - assert.strictEqual(chris.name, 'Chris', 'PreCond: We have chris'); - assert.strictEqual(john.name, 'John', 'PreCond: We have john'); + assert.equal(chris.name, 'Chris', 'PreCond: We have chris'); + assert.equal(john.name, 'John', 'PreCond: We have john'); assert.false(chris.isDeleted, 'PreCond: Chris is not deleted'); assert.false(john.isDeleted, 'PreCond: John is not deleted'); - const chrisState = stateOf(chrisBestFriend); - const johnState = stateOf(johnBestFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const chrisState = stateOf(store._graph, chrisBestFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const johnState = stateOf(store._graph, johnBestFriend); assert.deepEqual( chrisState.remote, @@ -186,14 +201,15 @@ export async function setInitialState(context: Context, config: TestConfig, asse const chrisImplicits = graph.getImplicit(chrisIdentifier); const johnImplicits = graph.getImplicit(johnIdentifier); - assert.strictEqual(Object.keys(chrisImplicits).length, 1, 'PreCond: Chris has one implicit relationship'); + assert.equal(Object.keys(chrisImplicits).length, 1, 'PreCond: Chris has one implicit relationship'); - const chrisImplicitFriend = chrisImplicits[chrisBestFriend.definition.inverseKey] as ImplicitRelationship; - const johnImplicitFriend = johnImplicits[johnBestFriend.definition.inverseKey] as ImplicitRelationship; + const chrisImplicitFriend = chrisImplicits[chrisBestFriend.definition.inverseKey]; + const johnImplicitFriend = johnImplicits[johnBestFriend.definition.inverseKey]; assert.ok(chrisImplicitFriend, 'PreCond: Chris has an implicit best friend'); - const chrisImplicitState = stateOf(chrisImplicitFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const chrisImplicitState = stateOf(store._graph, chrisImplicitFriend); assert.deepEqual( chrisImplicitState.remote, @@ -211,12 +227,13 @@ export async function setInitialState(context: Context, config: TestConfig, asse // implicits on john are managed by chris, so with inverseNull // the implicit on john will be empty since chris should have no state. if (config.useCreate) { - assert.strictEqual(Object.keys(johnImplicits).length, 0, 'PreCond: John has no implicit relationship'); + assert.equal(Object.keys(johnImplicits).length, 0, 'PreCond: John has no implicit relationship'); assert.notOk(johnImplicitFriend, 'PreCond: John has no implicit best friend'); } else { - assert.strictEqual(Object.keys(johnImplicits).length, 1, 'PreCond: John has one implicit relationship'); + assert.equal(Object.keys(johnImplicits).length, 1, 'PreCond: John has one implicit relationship'); assert.ok(johnImplicitFriend, 'PreCond: John has no implicit best friend'); - const johnImplicitState = stateOf(johnImplicitFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const johnImplicitState = stateOf(store._graph, johnImplicitFriend); assert.deepEqual( johnImplicitState.remote, config.dirtyLocal || config.useCreate ? [] : [chrisIdentifier], @@ -247,22 +264,23 @@ export async function setInitialState(context: Context, config: TestConfig, asse }; } -export async function testFinalState( +export function testFinalState( context: Context, testState: TestState, config: TestConfig, statuses: ExpectedTestOutcomes, - assert + assert: Diagnostic ) { - const { graph } = context; + const { graph, store } = context; const { chrisIdentifier, johnIdentifier } = testState; const chrisBestFriend = graph.get(chrisIdentifier, 'bestFriends'); - const chrisState = stateOf(chrisBestFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const chrisState = stateOf(store._graph, chrisBestFriend); // this specific case gets it's own WAT // this is something ideally a refactor should do away with. - const isUnloadOfImplictAsyncHasManyWithLocalChange = + const isUnloadOfImplicitAsyncHasManyWithLocalChange = !!config.isUnloadAsDelete && !!config.dirtyLocal && !!config.async && @@ -275,7 +293,7 @@ export async function testFinalState( // in the dirtyLocal and useCreate case there is no remote data const chrisRemoteRemoved = config.dirtyLocal || config.useCreate || statuses.removed; - const chrisLocalRemoved = statuses.removed && !isUnloadOfImplictAsyncHasManyWithLocalChange; + const chrisLocalRemoved = statuses.removed && !isUnloadOfImplicitAsyncHasManyWithLocalChange; // for the isUnloadAsDelete case we don't remove unless dirtyLocal or useCreate // this may be a bug but likely is related to retaining info for rematerialization. @@ -325,7 +343,8 @@ export async function testFinalState( assert.false(graph.identifiers.has(johnIdentifier), 'Result: Relationships for John were cleared from the cache'); } else { const johnBestFriend = graph.get(johnIdentifier, 'bestFriends'); - const johnState = stateOf(johnBestFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const johnState = stateOf(store._graph, johnBestFriend); assert.deepEqual( johnState.remote, @@ -346,12 +365,13 @@ export async function testFinalState( if (config.inverseNull) { const chrisImplicits = graph.getImplicit(chrisIdentifier); - assert.strictEqual(Object.keys(chrisImplicits).length, 1, 'Result: Chris has one implicit relationship key'); + assert.equal(Object.keys(chrisImplicits).length, 1, 'Result: Chris has one implicit relationship key'); - const chrisImplicitFriend = chrisImplicits[testState.chrisInverseKey] as ImplicitRelationship; + const chrisImplicitFriend = chrisImplicits[testState.chrisInverseKey]; assert.ok(chrisImplicitFriend, 'Result: Chris has an implicit relationship for best friend'); - const chrisImplicitState = stateOf(chrisImplicitFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const chrisImplicitState = stateOf(store._graph, chrisImplicitFriend); assert.deepEqual( chrisImplicitState.remote, @@ -372,14 +392,11 @@ export async function testFinalState( assert.false(graph.implicit.has(johnIdentifier), 'implicit cache for john has been removed'); } else { const johnImplicits = graph.getImplicit(johnIdentifier); - const johnImplicitFriend = johnImplicits[testState.johnInverseKey] as ImplicitRelationship; - assert.strictEqual( - Object.keys(johnImplicits).length, - 1, - 'Result: John has one implicit relationship in the cache' - ); + const johnImplicitFriend = johnImplicits[testState.johnInverseKey]; + assert.equal(Object.keys(johnImplicits).length, 1, 'Result: John has one implicit relationship in the cache'); assert.ok(johnImplicitFriend, 'Result: John has an implicit key for best friend'); - const johnImplicitState = stateOf(johnImplicitFriend); + // @ts-expect-error TODO: Graph type is not assignable to private Graph type + const johnImplicitState = stateOf(store._graph, johnImplicitFriend); assert.deepEqual( johnImplicitState.remote, diff --git a/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts b/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts new file mode 100644 index 00000000000..2ef723e613b --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts @@ -0,0 +1,147 @@ +import type { CollectionEdge, Graph, GraphEdge, ImplicitEdge, ResourceEdge } from '@ember-data/graph/-private'; +import { graphFor } from '@ember-data/graph/-private'; +import type Model from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { Type } from '@warp-drive/core-types/symbols'; +import type { Hooks } from '@warp-drive/diagnostic/-types'; +import type { RenderingTestContext } from '@warp-drive/diagnostic/ember'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class AbstractMap { + constructor( + private store: Store, + // eslint-disable-next-line @typescript-eslint/no-shadow + private isImplicit: boolean + ) {} + + has(identifier: StableRecordIdentifier) { + const graph = graphFor(this.store); + return graph.identifiers.has(identifier); + } +} + +class AbstractGraph { + public identifiers: AbstractMap; + public implicit: { has(identifier: StableRecordIdentifier): boolean }; + + constructor(private store: Store) { + this.identifiers = new AbstractMap(store, false); + this.implicit = { + has: (identifier) => { + return Object.keys(this.getImplicit(identifier)).length > 0; + }, + }; + } + + get(identifier: StableRecordIdentifier, propertyName: string): GraphEdge { + return graphFor(this.store).get(identifier, propertyName); + } + + getImplicit(identifier: StableRecordIdentifier): Record { + const rels = graphFor(this.store).identifiers.get(identifier); + const implicits = Object.create(null) as Record; + if (rels) { + Object.keys(rels).forEach((key) => { + const rel = rels[key]; + if (rel && isImplicit(rel)) { + implicits[key] = rel; + } + }); + } + return implicits; + } +} + +function graphForTest(store: Store) { + return new AbstractGraph(store); +} + +export function isBelongsTo(relationship: GraphEdge): relationship is ResourceEdge { + return relationship.definition.kind === 'belongsTo'; +} + +export function isImplicit(relationship: GraphEdge): relationship is ImplicitEdge { + return relationship.definition.isImplicit; +} + +export function isHasMany(relationship: GraphEdge): relationship is CollectionEdge { + return relationship.definition.kind === 'hasMany'; +} + +function setToArray(set: Set): T[] { + return Array.from(set); +} + +export function stateOf( + graph: Graph, + rel: GraphEdge +): { + remote: StableRecordIdentifier[]; + local: StableRecordIdentifier[]; +} { + let local: StableRecordIdentifier[]; + let remote: StableRecordIdentifier[]; + + if (isBelongsTo(rel)) { + // we cast these to array form to make the tests more legible + local = rel.localState ? [rel.localState] : []; + remote = rel.remoteState ? [rel.remoteState] : []; + } else if (isHasMany(rel)) { + // ensure we calculate what is otherwise lazy + const data = graph.getData(rel.identifier, rel.definition.key) as CollectionRelationship; + local = data.data || []; + remote = rel.remoteState; + } else { + local = setToArray(rel.localMembers); + remote = setToArray(rel.remoteMembers); + } + return { + local, + remote, + }; +} + +class Adapter { + static create() { + return new this(); + } + static updateRecord() { + return Promise.resolve(); + } + async deleteRecord() { + return Promise.resolve({ data: null }); + } +} +class Serializer { + static create() { + return new this(); + } + normalizeResponse(_: Store, __: ModelSchema, data: unknown) { + return data; + } +} + +export type UserRecord = Model & { + name?: string; + bestFriend?: UserRecord; + bestFriends?: UserRecord[]; + [Type]: 'user'; +}; + +export interface Context extends RenderingTestContext { + store: Store; + graph: AbstractGraph; +} + +export function setupGraphTest(hooks: Hooks) { + setupTest(hooks); + hooks.beforeEach(function (this: Context) { + this.owner.register('adapter:application', Adapter); + this.owner.register('serializer:application', Serializer); + this.store = this.owner.lookup('service:store') as Store; + this.graph = graphForTest(this.store); + }); +} diff --git a/tests/graph/tests/integration/graph/edge-test.ts b/tests/ember-data__graph/tests/integration/graph/edge-test.ts similarity index 82% rename from tests/graph/tests/integration/graph/edge-test.ts rename to tests/ember-data__graph/tests/integration/graph/edge-test.ts index 4cdecf74804..31c146d577d 100644 --- a/tests/graph/tests/integration/graph/edge-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-test.ts @@ -1,21 +1,23 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - +import type { Graph } from '@ember-data/graph/-private'; import { graphFor } from '@ember-data/graph/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { peekCache, recordIdentifierFor } from '@ember-data/store/-private'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { peekCache } from '@ember-data/store/-private'; +import { Type } from '@warp-drive/core-types/symbols'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; import { stateOf } from './edge-removal/setup'; module('Integration | Graph | Edges', function (hooks) { setupTest(hooks); - let store; - let graph; + let store: Store; + let graph: Graph; hooks.beforeEach(function () { const { owner } = this; - store = owner.lookup('service:store'); + store = owner.lookup('service:store') as Store; graph = graphFor(store); }); @@ -27,12 +29,13 @@ module('Integration | Graph | Edges', function (hooks) { * knowledge derived from it's inverses. */ - test('accessing the relationships for an identifier does not instantiate record-data for that identifier', async function (assert) { + test('accessing the relationships for an identifier does not instantiate record-data for that identifier', function (assert) { const { owner } = this; const { identifierCache } = store; class User extends Model { - @attr name; - @belongsTo('user', { async: false, inverse: 'bestFriend' }) bestFriend; + @attr declare name: string; + @belongsTo('user', { async: false, inverse: 'bestFriend' }) declare bestFriend: User; + [Type] = 'user' as const; } owner.register('model:user', User); @@ -40,7 +43,7 @@ module('Integration | Graph | Edges', function (hooks) { const identifier2 = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); const bestFriend = graph.get(identifier, 'bestFriend'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We have no record data instance afer accessing the relationships for this identifier' @@ -48,13 +51,13 @@ module('Integration | Graph | Edges', function (hooks) { assert.ok(bestFriend, 'We can access a specific relationship'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after accessing a named relationship' ); - store.push({ + store.push({ data: { type: 'user', id: '2', @@ -63,17 +66,17 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after push of only an identifier within a relationship' ); - let state = stateOf(bestFriend); + const state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', @@ -81,21 +84,21 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); - assert.strictEqual(record.name, 'Chris', 'We have the right name'); - assert.strictEqual(recordIdentifierFor(record), identifier, 'The identifiers are equivalent'); + assert.equal(record.name, 'Chris', 'We have the right name'); + assert.equal(recordIdentifierFor(record), identifier, 'The identifiers are equivalent'); }); - test('working with a sync belongsTo relationship for an identifier does not instantiate record-data for that identifier', async function (assert) { + test('working with a sync belongsTo relationship for an identifier does not instantiate record-data for that identifier', function (assert) { const { owner } = this; const { identifierCache } = store; class User extends Model { - @attr name; - @belongsTo('user', { async: false, inverse: 'bestFriend' }) bestFriend; + @attr declare name: string; + @belongsTo('user', { async: false, inverse: 'bestFriend' }) declare bestFriend: User; } owner.register('model:user', User); @@ -112,13 +115,13 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); - let state = stateOf(bestFriend); + let state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); @@ -132,11 +135,11 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriend); + state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after canonical update'); assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' @@ -151,15 +154,11 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriend); + state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after local update'); assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); - assert.strictEqual( - peekCache(identifier), - null, - 'We still have no record data instance after updating the local state' - ); + assert.equal(peekCache(identifier), null, 'We still have no record data instance after updating the local state'); store.push({ data: { @@ -169,19 +168,19 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); }); - test('working with an async belongsTo relationship for an identifier does not instantiate record-data for that identifier', async function (assert) { + test('working with an async belongsTo relationship for an identifier does not instantiate record-data for that identifier', function (assert) { const { owner } = this; const { identifierCache } = store; class User extends Model { - @attr name; - @belongsTo('user', { async: true, inverse: 'bestFriend' }) bestFriend; + @attr declare name: string; + @belongsTo('user', { async: true, inverse: 'bestFriend' }) declare bestFriend: Promise; } owner.register('model:user', User); @@ -198,13 +197,13 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); - let state = stateOf(bestFriend); + let state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); @@ -218,11 +217,11 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriend); + state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after canonical update'); assert.deepEqual(state.local, [identifier3], 'Our current state is correct after canonical update'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' @@ -237,15 +236,11 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriend); + state = stateOf(graph, bestFriend); assert.deepEqual(state.remote, [identifier3], 'Our canonical state is correct after local update'); assert.deepEqual(state.local, [identifier2], 'Our current state is correct after local update'); - assert.strictEqual( - peekCache(identifier), - null, - 'We still have no record data instance after updating the local state' - ); + assert.equal(peekCache(identifier), null, 'We still have no record data instance after updating the local state'); store.push({ data: { @@ -255,19 +250,19 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); }); - test('working with a sync hasMany relationship for an identifier does not instantiate record-data for that identifier', async function (assert) { + test('working with a sync hasMany relationship for an identifier does not instantiate record-data for that identifier', function (assert) { const { owner } = this; const { identifierCache } = store; class User extends Model { - @attr name; - @hasMany('user', { async: false, inverse: 'bestFriends' }) bestFriends; + @attr declare name: string; + @hasMany('user', { async: false, inverse: 'bestFriends' }) declare bestFriends: User[]; } owner.register('model:user', User); @@ -289,13 +284,13 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); - let state = stateOf(bestFriends); + let state = stateOf(graph, bestFriends); assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); @@ -308,7 +303,7 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriends); + state = stateOf(graph, bestFriends); assert.deepEqual( state.remote, [identifier2, identifier3], @@ -316,7 +311,7 @@ module('Integration | Graph | Edges', function (hooks) { ); assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' @@ -332,7 +327,7 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriends); + state = stateOf(graph, bestFriends); assert.deepEqual(state.remote, [identifier2, identifier3], 'Our canonical state is correct after local update'); assert.deepEqual( state.local, @@ -340,11 +335,7 @@ module('Integration | Graph | Edges', function (hooks) { 'Our current state is correct after local update' ); - assert.strictEqual( - peekCache(identifier), - null, - 'We still have no record data instance after updating the local state' - ); + assert.equal(peekCache(identifier), null, 'We still have no record data instance after updating the local state'); store.push({ data: { @@ -354,19 +345,19 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' ); }); - test('working with an async hasMany relationship for an identifier does not instantiate record-data for that identifier', async function (assert) { + test('working with an async hasMany relationship for an identifier does not instantiate record-data for that identifier', function (assert) { const { owner } = this; const { identifierCache } = store; class User extends Model { - @attr name; - @hasMany('user', { async: true, inverse: 'bestFriends' }) bestFriends; + @attr declare name: string; + @hasMany('user', { async: true, inverse: 'bestFriends' }) declare bestFriends: Promise; } owner.register('model:user', User); @@ -388,13 +379,13 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We have no record data instance after push of only an identifier within a relationship' ); - let state = stateOf(bestFriends); + let state = stateOf(graph, bestFriends); assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); @@ -407,7 +398,7 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriends); + state = stateOf(graph, bestFriends); assert.deepEqual( state.remote, [identifier2, identifier3], @@ -415,7 +406,7 @@ module('Integration | Graph | Edges', function (hooks) { ); assert.deepEqual(state.local, [identifier2, identifier3], 'Our current state is correct after canonical update'); - assert.strictEqual( + assert.equal( peekCache(identifier), null, 'We still have no record data instance after updating the canonical state' @@ -431,7 +422,7 @@ module('Integration | Graph | Edges', function (hooks) { }); }); - state = stateOf(bestFriends); + state = stateOf(graph, bestFriends); assert.deepEqual(state.remote, [identifier2, identifier3], 'Our canonical state is correct after local update'); assert.deepEqual( state.local, @@ -439,11 +430,7 @@ module('Integration | Graph | Edges', function (hooks) { 'Our current state is correct after local update' ); - assert.strictEqual( - peekCache(identifier), - null, - 'We still have no record data instance after updating the local state' - ); + assert.equal(peekCache(identifier), null, 'We still have no record data instance after updating the local state'); store.push({ data: { @@ -453,7 +440,7 @@ module('Integration | Graph | Edges', function (hooks) { }, }); - assert.strictEqual( + assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), 'Chris', 'We lazily associate the correct record data instance' diff --git a/tests/ember-data__graph/tests/integration/graph/graph-test.ts b/tests/ember-data__graph/tests/integration/graph/graph-test.ts new file mode 100644 index 00000000000..0a5eb668094 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/graph-test.ts @@ -0,0 +1,96 @@ +import { graphFor } from '@ember-data/graph/-private'; +import Store from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('Integration | Graph | Configuration', function (hooks) { + setupTest(hooks); + + class MyStore extends Store { + isGraphStore = true; + } + + let store: MyStore; + hooks.beforeEach(function (assert) { + const { owner } = this; + owner.register('service:store', MyStore); + store = owner.lookup('service:store') as MyStore; + assert.equal(store.isGraphStore, true, 'pre-cond, store registered correctly'); + }); + + test('graphFor util returns the same graph instance for repeated calls on the same store wrapper instance', function (assert) { + const wrapper = store._instanceCache._storeWrapper; + const graph1 = graphFor(wrapper); + const graph2 = graphFor(wrapper); + const graph3 = graphFor(wrapper); + + assert.equal(graph1, graph2, 'We got the same instance the second time'); + assert.equal(graph2, graph3, 'We got the same instance the third time'); + }); + + test('graphFor util returns a new graph instance for each unique store wrapper', function (assert) { + const { owner } = this; + const wrapper1 = store._instanceCache._storeWrapper; + + owner.register('service:store2', MyStore); + owner.register('service:store3', MyStore); + + const store2 = owner.lookup('service:store2') as unknown as Store; + const store3 = owner.lookup('service:store3') as unknown as Store; + const wrapper2 = store2._instanceCache._storeWrapper; + const wrapper3 = store3._instanceCache._storeWrapper; + + const graph1 = graphFor(wrapper1); + const graph2 = graphFor(wrapper2); + const graph3 = graphFor(wrapper3); + + assert.notEqual(graph1, graph2, 'We got a new instance for store2'); + assert.notEqual(graph1, graph3, 'We got a new instance for store3'); + assert.notEqual(graph2, graph3, 'The instance for store2 is not the same as store3'); + }); + + test('graphFor util returns the same graph instance for repeated calls on the same store instance', function (assert) { + const graph1 = graphFor(store); + const graph2 = graphFor(store); + const graph3 = graphFor(store); + + assert.equal(graph1, graph2, 'We got the same instance the second time'); + assert.equal(graph2, graph3, 'We got the same instance the third time'); + }); + + test('graphFor util returns a new graph instance for each unique store', function (assert) { + const { owner } = this; + owner.register('service:store2', MyStore); + owner.register('service:store3', MyStore); + + const store2 = owner.lookup('service:store2') as unknown as Store; + const store3 = owner.lookup('service:store3') as unknown as Store; + + const graph1 = graphFor(store); + const graph2 = graphFor(store2); + const graph3 = graphFor(store3); + + assert.notEqual(graph1, graph2, 'We got a new instance for store2'); + assert.notEqual(graph1, graph3, 'We got a new instance for store3'); + assert.notEqual(graph2, graph3, 'The instance for store2 is not the same as store3'); + }); + + test('graphFor util returns the same graph instance for the store and storeWrapper', function (assert) { + const { owner } = this; + const wrapper = store._instanceCache._storeWrapper; + // lookup the wrapper first + const graph1 = graphFor(wrapper); + const graph2 = graphFor(store); + + owner.register('service:store2', MyStore); + const store2 = owner.lookup('service:store2') as unknown as Store; + const wrapper2 = store2._instanceCache._storeWrapper; + // lookup the store first + const graph3 = graphFor(store2); + const graph4 = graphFor(wrapper2); + + assert.equal(graph1, graph2, 'We got the same instance when wrapper is looked up first'); + assert.equal(graph3, graph4, 'We got the same instance when store is looked up first'); + assert.notEqual(graph1, graph3, 'The stores do not share an instance'); + }); +}); diff --git a/tests/ember-data__graph/tests/integration/graph/order-preservation-test.ts b/tests/ember-data__graph/tests/integration/graph/order-preservation-test.ts new file mode 100644 index 00000000000..9b2c6d42e24 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/order-preservation-test.ts @@ -0,0 +1,706 @@ +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +class App extends Model { + @attr declare name: string; + @hasMany('config', { async: false, inverse: 'app' }) declare configs: Config[]; + @belongsTo('cluster', { async: false, inverse: 'apps' }) declare cluster: Cluster | null; + @hasMany('group', { async: false, inverse: 'apps' }) declare groups: Groups[]; +} + +class Config extends Model { + @attr declare name: string; + @belongsTo('app', { async: false, inverse: 'configs' }) declare app: App | null; +} + +class Cluster extends Model { + @attr declare name: string; + @hasMany('app', { async: false, inverse: 'cluster' }) declare apps: App[]; +} + +class Groups extends Model { + @attr declare name: string; + @hasMany('app', { async: false, inverse: 'groups' }) declare apps: App[]; +} + +module('Graph | Order Preservation', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + + owner.register('model:app', App); + owner.register('model:config', Config); + owner.register('model:cluster', Cluster); + owner.register('model:group', Groups); + }); + + module('during local mutation', function (innerHooks) { + innerHooks.beforeEach(function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + const clusterIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'cluster', id: '1' }); + + // setup initial state + // app 1 has configs 1, 2, 3 + // app 1 is in cluster 1 + // cluster 1 has apps 1, 2, 3 + // app 1 is in groups 1, 2, 3 + // each group has apps 1, 2, 3 + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'configs', + record: appIdentifier, + value: { + data: [ + { type: 'config', id: '1' }, + { type: 'config', id: '2' }, + { type: 'config', id: '3' }, + ], + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'cluster', + record: appIdentifier, + value: { + data: { type: 'cluster', id: '1' }, + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: clusterIdentifier, + value: { + data: [ + { type: 'app', id: '1' }, + { type: 'app', id: '2' }, + { type: 'app', id: '3' }, + ], + }, + }); + graph.push({ + op: 'updateRelationship', + field: 'groups', + record: appIdentifier, + value: { + data: [ + { type: 'group', id: '1' }, + { type: 'group', id: '2' }, + { type: 'group', id: '3' }, + ], + }, + }); + ['1', '2', '3'].forEach((id) => { + const groupIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'group', id }); + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: groupIdentifier, + value: { + data: [ + { type: 'app', id: '1' }, + { type: 'app', id: '2' }, + { type: 'app', id: '3' }, + ], + }, + }); + }); + }); + + // flush initial state to localState + graph.getData(appIdentifier, 'configs'); + graph.getData(appIdentifier, 'cluster'); + graph.getData(clusterIdentifier, 'apps'); + graph.getData(appIdentifier, 'groups'); + ['1', '2', '3'].forEach((id) => { + const groupIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'group', id }); + graph.getData(groupIdentifier, 'apps'); + }); + + assert.watchNotifications(); + }); + + test('order is preserved when doing a full replace of a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // change the order of configs + // from '1', '2', '3' + // to '3', '1', '2' + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '1'), 'relationships', 'app', 0); + assert.notified(identifier('config', '2'), 'relationships', 'app', 0); + assert.notified(identifier('config', '3'), 'relationships', 'app', 0); + + const configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + 'we have the expected order' + ); + + // change the order of groups + // from '1', '2', '3' + // to '3', '1', '2' + // this should not affect ordering within the groups + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'groups', + record: appIdentifier, + value: [identifier('group', '3'), identifier('group', '1'), identifier('group', '2')], + }); + }); + + assert.notified(appIdentifier, 'relationships', 'groups', 1); + assert.notified(identifier('group', '1'), 'relationships', 'app', 0); + assert.notified(identifier('group', '2'), 'relationships', 'app', 0); + assert.notified(identifier('group', '3'), 'relationships', 'app', 0); + + const groupState = graph.getData(appIdentifier, 'groups'); + assert.arrayStrictEquals( + groupState.data, + [identifier('group', '3'), identifier('group', '1'), identifier('group', '2')], + 'we have the expected order' + ); + ['1', '2', '3'].forEach((id) => { + const groupIdentifier = identifier('group', id); + const groupAppsState = graph.getData(groupIdentifier, 'apps'); + assert.arrayStrictEquals( + groupAppsState.data, + [identifier('app', '1'), identifier('app', '2'), identifier('app', '3')], + `group ${id} has the expected order` + ); + }); + }); + + test('order is preserved when adding to a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // add a new config '4' without an index + store._join(() => { + graph.update({ + op: 'addToRelatedRecords', + field: 'configs', + record: appIdentifier, + value: identifier('config', '4'), + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '4'), 'relationships', 'app', 1); + + let configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3'), identifier('config', '4')], + 'we have the expected order' + ); + + // add a new config '5' with an index + store._join(() => { + graph.update({ + op: 'addToRelatedRecords', + field: 'configs', + record: appIdentifier, + value: identifier('config', '5'), + index: 1, + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '5'), 'relationships', 'app', 1); + + configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [ + identifier('config', '1'), + identifier('config', '5'), + identifier('config', '2'), + identifier('config', '3'), + identifier('config', '4'), + ], + 'we have the expected order' + ); + + // setup group 4 with apps 2, 3, 4 + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: identifier('group', '4'), + value: { + data: [identifier('app', '2'), identifier('app', '3'), identifier('app', '4')], + }, + }); + }); + + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + + // assert starting state + let appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '2'), identifier('app', '3'), identifier('app', '4')], + 'we have the expected order' + ); + + // mutate group 4 order to 3, 4, 2 + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'apps', + record: identifier('group', '4'), + value: [identifier('app', '3'), identifier('app', '4'), identifier('app', '2')], + }); + }); + + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + + // assert mutated state + appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), identifier('app', '4'), identifier('app', '2')], + 'we have the expected order' + ); + + // add a group '4' to app '1' + store._join(() => { + graph.update({ + op: 'addToRelatedRecords', + field: 'groups', + record: appIdentifier, + value: identifier('group', '4'), + }); + }); + + assert.notified(appIdentifier, 'relationships', 'groups', 1); + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + + // assert mutated state + const groupsState = graph.getData(appIdentifier, 'groups'); + assert.arrayStrictEquals( + groupsState.data, + [identifier('group', '1'), identifier('group', '2'), identifier('group', '3'), identifier('group', '4')], + 'we have the expected order' + ); + + // assert group 4 has the expected order + appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), identifier('app', '4'), identifier('app', '2'), identifier('app', '1')], + 'we have the expected order' + ); + }); + + test('order is preserved when removing from a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // remove config '2' + store._join(() => { + graph.update({ + op: 'removeFromRelatedRecords', + field: 'configs', + record: appIdentifier, + value: identifier('config', '2'), + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '2'), 'relationships', 'app', 1); + + let configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '1'), identifier('config', '3')], + 'we have the expected order' + ); + + // add config '2' back to the end + store._join(() => { + graph.update({ + op: 'addToRelatedRecords', + field: 'configs', + record: appIdentifier, + value: identifier('config', '2'), + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '2'), 'relationships', 'app', 1); + + configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '1'), identifier('config', '3'), identifier('config', '2')], + 'we have the expected order' + ); + + // remove config '3' with an index + store._join(() => { + graph.update({ + op: 'removeFromRelatedRecords', + field: 'configs', + record: appIdentifier, + value: identifier('config', '3'), + index: 1, + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '3'), 'relationships', 'app', 1); + + configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '1'), identifier('config', '2')], + 'we have the expected order' + ); + }); + + test('order is preserved when adding via the inverse hasMany of a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // setup group 4 with apps 2, 3, 4 + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: identifier('group', '4'), + value: { + data: [identifier('app', '2'), identifier('app', '3'), identifier('app', '4')], + }, + }); + }); + + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + assert.notified(identifier('app', '2'), 'relationships', 'groups', 1); + assert.notified(identifier('app', '3'), 'relationships', 'groups', 1); + assert.notified(identifier('app', '4'), 'relationships', 'groups', 1); + + // assert starting state + let appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '2'), identifier('app', '3'), identifier('app', '4')], + 'we have the expected order' + ); + + // mutate group 4 order to 3, 4, 2 + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'apps', + record: identifier('group', '4'), + value: [identifier('app', '3'), identifier('app', '4'), identifier('app', '2')], + }); + }); + + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + assert.notified(identifier('app', '2'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '3'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '4'), 'relationships', 'groups', 0); + + // assert mutated state + appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), identifier('app', '4'), identifier('app', '2')], + 'we have the expected order' + ); + + // add a group '4' to app '1' + store._join(() => { + graph.update({ + op: 'addToRelatedRecords', + field: 'groups', + record: appIdentifier, + value: identifier('group', '4'), + }); + }); + + assert.notified(appIdentifier, 'relationships', 'groups', 1); + assert.notified(identifier('group', '4'), 'relationships', 'apps', 1); + + // assert mutated state + const groupsState = graph.getData(appIdentifier, 'groups'); + assert.arrayStrictEquals( + groupsState.data, + [identifier('group', '1'), identifier('group', '2'), identifier('group', '3'), identifier('group', '4')], + 'we have the expected order' + ); + + // assert group 4 has the expected order + appsState = graph.getData(identifier('group', '4'), 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), identifier('app', '4'), identifier('app', '2'), identifier('app', '1')], + 'we have the expected order' + ); + }); + + test('order is preserved when removing via the inverse hasMany of a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + const groupIdentifier = identifier('group', '3'); + + // setup group 3 with apps 2, 1, 3, 4 + store._join(() => { + graph.push({ + op: 'updateRelationship', + field: 'apps', + record: groupIdentifier, + value: { + data: [identifier('app', '2'), appIdentifier, identifier('app', '3'), identifier('app', '4')], + }, + }); + }); + + assert.notified(groupIdentifier, 'relationships', 'apps', 1); + assert.notified(appIdentifier, 'relationships', 'groups', 0); + assert.notified(identifier('app', '2'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '3'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '4'), 'relationships', 'groups', 1); + + // assert starting state + let appsState = graph.getData(groupIdentifier, 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '2'), appIdentifier, identifier('app', '3'), identifier('app', '4')], + 'we have the expected order' + ); + + // mutate group 3 order to 3, 1, 4, 2 + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'apps', + record: groupIdentifier, + value: [identifier('app', '3'), appIdentifier, identifier('app', '4'), identifier('app', '2')], + }); + }); + + assert.notified(groupIdentifier, 'relationships', 'apps', 1); + assert.notified(appIdentifier, 'relationships', 'groups', 0); + assert.notified(identifier('app', '2'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '3'), 'relationships', 'groups', 0); + assert.notified(identifier('app', '4'), 'relationships', 'groups', 0); + + // assert mutated state + appsState = graph.getData(groupIdentifier, 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), appIdentifier, identifier('app', '4'), identifier('app', '2')], + 'we have the expected order' + ); + + // now, remove group 3 from app 1 + store._join(() => { + graph.update({ + op: 'removeFromRelatedRecords', + field: 'groups', + record: appIdentifier, + value: groupIdentifier, + }); + }); + + assert.notified(appIdentifier, 'relationships', 'groups', 1); + assert.notified(groupIdentifier, 'relationships', 'apps', 1); + + // assert mutated state + const groupsState = graph.getData(appIdentifier, 'groups'); + assert.arrayStrictEquals( + groupsState.data, + [identifier('group', '1'), identifier('group', '2')], + 'we have the expected order' + ); + + // assert group 3 has the expected order + appsState = graph.getData(groupIdentifier, 'apps'); + assert.arrayStrictEquals( + appsState.data, + [identifier('app', '3'), identifier('app', '4'), identifier('app', '2')], + 'we have the expected order' + ); + }); + + test('order is preserved when adding via the inverse belongsTo of a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // change the order of configs + // from '1', '2', '3' + // to '3', '1', '2' + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '1'), 'relationships', 'app', 0); + assert.notified(identifier('config', '2'), 'relationships', 'app', 0); + assert.notified(identifier('config', '3'), 'relationships', 'app', 0); + + const configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + 'we have the expected order' + ); + + // add a new config '4' + store._join(() => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', '4'), + value: appIdentifier, + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '4'), 'relationships', 'app', 1); + + // assert mutated state + const config4State = graph.getData(identifier('config', '4'), 'app'); + assert.equal(config4State.data, appIdentifier, 'config 4 has the expected app'); + + const configState2 = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState2.data, + [identifier('config', '3'), identifier('config', '1'), identifier('config', '2'), identifier('config', '4')], + 'we have the expected order' + ); + }); + + test('order is preserved when removing via the inverse belongsTo of a hasMany', function (assert) { + const { owner } = this; + const store = owner.lookup('service:store') as Store; + const graph = graphFor(store); + + function identifier(type: string, id: string) { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const appIdentifier = identifier('app', '1'); + + // change the order of configs + // from '1', '2', '3' + // to '3', '1', '2' + store._join(() => { + graph.update({ + op: 'replaceRelatedRecords', + field: 'configs', + record: appIdentifier, + value: [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '1'), 'relationships', 'app', 0); + assert.notified(identifier('config', '2'), 'relationships', 'app', 0); + assert.notified(identifier('config', '3'), 'relationships', 'app', 0); + + const configState = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState.data, + [identifier('config', '3'), identifier('config', '1'), identifier('config', '2')], + 'we have the expected order' + ); + + // remove config '1' + store._join(() => { + graph.update({ + op: 'replaceRelatedRecord', + field: 'app', + record: identifier('config', '1'), + value: null, + }); + }); + + assert.notified(appIdentifier, 'relationships', 'configs', 1); + assert.notified(identifier('config', '1'), 'relationships', 'app', 1); + + // assert mutated state + const config1State = graph.getData(identifier('config', '1'), 'app'); + assert.equal(config1State.data, null, 'config 1 has the expected app'); + + const configState2 = graph.getData(appIdentifier, 'configs'); + assert.arrayStrictEquals( + configState2.data, + [identifier('config', '3'), identifier('config', '2')], + 'we have the expected order' + ); + }); + }); +}); diff --git a/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts b/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts new file mode 100644 index 00000000000..ce6c27a2d29 --- /dev/null +++ b/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts @@ -0,0 +1,77 @@ +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, belongsTo } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import { Type } from '@warp-drive/core-types/symbols'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('Integration | Graph | Implicit Keys', function (hooks) { + setupTest(hooks); + + test('Non-polymorphic records do not trigger polymorphic assertions when they share the same key with another record', async function (assert) { + const { owner } = this; + class User extends Model { + @attr declare name: string; + @belongsTo('organization', { async: false, inverse: null }) declare organization: Organization; + [Type] = 'user' as const; + } + class Product extends Model { + @attr declare name: string; + @belongsTo('organization', { async: false, inverse: null }) declare organization: Organization; + [Type] = 'product' as const; + } + class Organization extends Model { + @attr declare name: string; + [Type] = 'organization' as const; + } + owner.register('model:user', User); + owner.register('model:product', Product); + owner.register('model:organization', Organization); + + const store = owner.lookup('service:store') as unknown as Store; + const graph = graphFor(store); + let user!: User, product!: Product, organization!: Organization; + + await assert.expectNoAssertion(() => { + const data: CollectionResourceDocument<'user' | 'product' | 'organization'> = { + data: [ + { + type: 'user', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + organization: { data: { type: 'organization', id: '1 ' } }, + }, + }, + { + type: 'product', + id: '1', + attributes: { name: 'Awesome Relationships' }, + relationships: { + organization: { data: { type: 'organization', id: '1 ' } }, + }, + }, + { + type: 'organization', + id: '1', + attributes: { name: 'Ember.js' }, + }, + ], + }; + [user, product, organization] = store.push(data) as [User, Product, Organization]; + }); + + const userIdentifier = recordIdentifierFor(user); + const productIdentifier = recordIdentifierFor(product); + const organizationIdentifier = recordIdentifierFor(organization); + + const userOrg = graph.get(userIdentifier, 'organization'); + const userImpl = graph.get(organizationIdentifier, userOrg.definition.inverseKey); + const productOrg = graph.get(productIdentifier, 'organization'); + const productImpl = graph.get(organizationIdentifier, productOrg.definition.inverseKey); + + assert.notEqual(userImpl, productImpl, 'We have separate implicit caches'); + }); +}); diff --git a/tests/graph/tests/integration/graph/unload-test.ts b/tests/ember-data__graph/tests/integration/graph/unload-test.ts similarity index 81% rename from tests/graph/tests/integration/graph/unload-test.ts rename to tests/ember-data__graph/tests/integration/graph/unload-test.ts index e90aa7f8849..54a4b2abf52 100644 --- a/tests/graph/tests/integration/graph/unload-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/unload-test.ts @@ -1,13 +1,10 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - +import type { Graph, ResourceEdge } from '@ember-data/graph/-private'; import { graphFor } from '@ember-data/graph/-private'; -import type { Graph } from '@ember-data/graph/-private/graph/graph'; -import BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import Model, { attr, belongsTo } from '@ember-data/model'; import type Store from '@ember-data/store'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; module('Integration | Graph | Unload', function (hooks) { setupTest(hooks); @@ -51,16 +48,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -125,16 +122,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -213,16 +210,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -293,16 +290,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -387,16 +384,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -461,16 +458,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -549,16 +546,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -629,16 +626,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -723,16 +720,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -797,16 +794,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -885,16 +882,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); if (unloadTogether) { store._join(() => { @@ -965,16 +962,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend = graph.get(identifier, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend = graph.get(identifier, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend.localState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend.localState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend.localState, identifier2, 'precond - bestFriend is set'); + assert.equal(bestFriend.remoteState, identifier2, 'precond - bestFriend is set'); + assert.equal(worstFriend.localState, identifier3, 'precond - worstFriend is set'); + assert.equal(worstFriend.remoteState, identifier3, 'precond - worstFriend is set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); const first = order[0]; const rest = order.slice(1); @@ -1060,16 +1057,16 @@ module('Integration | Graph | Unload', function (hooks) { }); }); - const bestFriend = graph.get(identifier, 'bestFriend') as BelongsToRelationship; - const bestFriend2 = graph.get(identifier2, 'bestFriend') as BelongsToRelationship; - const worstFriend3 = graph.get(identifier3, 'worstFriend') as BelongsToRelationship; + const bestFriend = graph.get(identifier, 'bestFriend') as ResourceEdge; + const bestFriend2 = graph.get(identifier2, 'bestFriend') as ResourceEdge; + const worstFriend3 = graph.get(identifier3, 'worstFriend') as ResourceEdge; - assert.strictEqual(bestFriend2.localState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); - assert.strictEqual(worstFriend3.localState, identifier, 'precond - worstFriend is set'); - assert.strictEqual(worstFriend3.remoteState, identifier, 'precond - worstFriend is set'); - assert.strictEqual(bestFriend.localState, null, 'precond - bestFriend is not set'); - assert.strictEqual(bestFriend.remoteState, null, 'precond - bestFriend is not set'); + assert.equal(bestFriend2.localState, identifier, 'precond - bestFriend is set'); + assert.equal(bestFriend2.remoteState, identifier, 'precond - bestFriend is set'); + assert.equal(worstFriend3.localState, identifier, 'precond - worstFriend is set'); + assert.equal(worstFriend3.remoteState, identifier, 'precond - worstFriend is set'); + assert.equal(bestFriend.localState, null, 'precond - bestFriend is not set'); + assert.equal(bestFriend.remoteState, null, 'precond - bestFriend is not set'); store._join(() => { graph.push({ diff --git a/tests/ember-data__graph/tests/setup-test.ts b/tests/ember-data__graph/tests/setup-test.ts new file mode 100644 index 00000000000..057e39a0896 --- /dev/null +++ b/tests/ember-data__graph/tests/setup-test.ts @@ -0,0 +1,4 @@ +import { createDeprecatedTestFn } from '@ember-data/unpublished-test-infra/test-support/test'; +import { skip, test } from '@warp-drive/diagnostic'; + +export const deprecatedTest = createDeprecatedTestFn({ skip, test }); diff --git a/tests/ember-data__graph/tests/test-helper.ts b/tests/ember-data__graph/tests/test-helper.ts new file mode 100644 index 00000000000..c380c8a49c8 --- /dev/null +++ b/tests/ember-data__graph/tests/test-helper.ts @@ -0,0 +1,24 @@ +import { setApplication } from '@ember/test-helpers'; + +import Application from 'ember-data__graph/app'; +import config from 'ember-data__graph/config/environment'; + +import configureAsserts from '@ember-data/unpublished-test-infra/test-support/asserts/index'; +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; + +setupGlobalHooks((hooks) => { + configureAsserts(hooks); +}); + +configure(); + +setApplication(Application.create(config.APP)); +void start({ + tryCatch: true, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/ember-data__graph/tsconfig.json b/tests/ember-data__graph/tsconfig.json new file mode 100644 index 00000000000..d299b914f1c --- /dev/null +++ b/tests/ember-data__graph/tsconfig.json @@ -0,0 +1,95 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "experimentalDecorators": true, + "noImplicitOverride": false, + "incremental": true, + "noEmit": true, + "declaration": false, + "paths": { + "ember-data__graph/*": ["./app/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/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/core-types": ["../../packages/core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../../packages/core-types/unstable-preview-types/*"], + "@warp-drive/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"] + }, + "types": ["ember-source/types"] + }, + "references": [ + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/diagnostic" + } + ] +} diff --git a/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.body.br b/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.body.br new file mode 100644 index 00000000000..8332a7f4e94 Binary files /dev/null and b/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.body.br differ diff --git a/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.meta.json b/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.meta.json new file mode 100644 index 00000000000..080c75c05cf --- /dev/null +++ b/tests/ember-data__json-api/.mock-cache/546c9e3a/GET-0-users/1/res.meta.json @@ -0,0 +1,12 @@ +{ + "url": "users/1", + "status": 404, + "statusText": "Not Found", + "headers": { + "Content-Type": "application/vnd.api+json", + "Content-Encoding": "br", + "Cache-Control": "no-store" + }, + "method": "GET", + "requestBody": null +} \ No newline at end of file diff --git a/tests/ember-data__json-api/README.md b/tests/ember-data__json-api/README.md new file mode 100644 index 00000000000..f1aa5615c8b --- /dev/null +++ b/tests/ember-data__json-api/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/json-api + +This test-package provides tests for the `@ember-data/json-api` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__json-api; +pnpm test; +``` diff --git a/tests/ember-data__json-api/app/app.ts b/tests/ember-data__json-api/app/app.ts new file mode 100644 index 00000000000..8f235d166e6 --- /dev/null +++ b/tests/ember-data__json-api/app/app.ts @@ -0,0 +1,16 @@ +import Application from '@ember/application'; + +import loadInitializers from 'ember-load-initializers'; + +import config from './config/environment'; +import Resolver from './resolver'; + +class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + override Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/tests/json-api/app/config/environment.d.ts b/tests/ember-data__json-api/app/config/environment.d.ts similarity index 100% rename from tests/json-api/app/config/environment.d.ts rename to tests/ember-data__json-api/app/config/environment.d.ts diff --git a/tests/ember-data__json-api/app/index.html b/tests/ember-data__json-api/app/index.html new file mode 100644 index 00000000000..28d5d1b2814 --- /dev/null +++ b/tests/ember-data__json-api/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/json-api Tests + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/request/app/resolver.ts b/tests/ember-data__json-api/app/resolver.ts similarity index 100% rename from tests/request/app/resolver.ts rename to tests/ember-data__json-api/app/resolver.ts diff --git a/tests/request/app/router.ts b/tests/ember-data__json-api/app/router.ts similarity index 100% rename from tests/request/app/router.ts rename to tests/ember-data__json-api/app/router.ts diff --git a/tests/json-api-encapsulation/app/styles/app.css b/tests/ember-data__json-api/app/styles/app.css similarity index 100% rename from tests/json-api-encapsulation/app/styles/app.css rename to tests/ember-data__json-api/app/styles/app.css diff --git a/tests/adapter-encapsulation/app/routes/.gitkeep b/tests/ember-data__json-api/app/templates/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/routes/.gitkeep rename to tests/ember-data__json-api/app/templates/.gitkeep diff --git a/tests/ember-data__json-api/app/templates/application.hbs b/tests/ember-data__json-api/app/templates/application.hbs new file mode 100644 index 00000000000..c24cd68950a --- /dev/null +++ b/tests/ember-data__json-api/app/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/tests/ember-data__json-api/config/environment.js b/tests/ember-data__json-api/config/environment.js new file mode 100644 index 00000000000..82ace648a37 --- /dev/null +++ b/tests/ember-data__json-api/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__json-api', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/graph/config/optional-features.json b/tests/ember-data__json-api/config/optional-features.json similarity index 100% rename from tests/graph/config/optional-features.json rename to tests/ember-data__json-api/config/optional-features.json diff --git a/tests/graph/config/targets.js b/tests/ember-data__json-api/config/targets.js similarity index 100% rename from tests/graph/config/targets.js rename to tests/ember-data__json-api/config/targets.js diff --git a/tests/ember-data__json-api/diagnostic.js b/tests/ember-data__json-api/diagnostic.js new file mode 100644 index 00000000000..28c6a933ba9 --- /dev/null +++ b/tests/ember-data__json-api/diagnostic.js @@ -0,0 +1,13 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; +import holodeck from '@warp-drive/holodeck'; + +await launch({ + async setup(options) { + await holodeck.launchProgram({ + port: options.port + 1, + }); + }, + async cleanup() { + await holodeck.endProgram(); + }, +}); diff --git a/tests/ember-data__json-api/ember-cli-build.js b/tests/ember-data__json-api/ember-cli-build.js new file mode 100644 index 00000000000..436ede741f6 --- /dev/null +++ b/tests/ember-data__json-api/ember-cli-build.js @@ -0,0 +1,28 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); + + return app.toTree(); +}; diff --git a/tests/ember-data__json-api/eslint.config.mjs b/tests/ember-data__json-api/eslint.config.mjs new file mode 100644 index 00000000000..aedceb2d799 --- /dev/null +++ b/tests/ember-data__json-api/eslint.config.mjs @@ -0,0 +1,28 @@ +// @ts-check +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 * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + diagnostic.browser({ + allowedImports: ['@ember/object'], + }), +]; diff --git a/tests/ember-data__json-api/package.json b/tests/ember-data__json-api/package.json new file mode 100644 index 00000000000..b601ec015a9 --- /dev/null +++ b/tests/ember-data__json-api/package.json @@ -0,0 +1,129 @@ +{ + "name": "ember-data__json-api", + "version": "4.12.8", + "private": true, + "description": "Provides tests for @ember-data/json-api", + "keywords": [], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "tests/ember-data__json-api" + }, + "license": "MIT", + "author": "", + "directories": { + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "start": "bun run build:tests --watch", + "build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "test": "bun ./diagnostic.js", + "test:production": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@warp-drive/diagnostic": { + "injected": true + }, + "@warp-drive/holodeck": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", + "@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/holodeck": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "@warp-drive/build-config": "workspace:*", + "ember-auto-import": "^2.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-test-loader": "^3.1.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "ember-source-channel-url": "^3.0.0", + "ember-try": "^3.0.0", + "loader.js": "^4.7.0", + "silent-error": "^1.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "ember": { + "edition": "octane" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } +} diff --git a/tests/adapter-encapsulation/app/templates/components/.gitkeep b/tests/ember-data__json-api/tests/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/app/templates/components/.gitkeep rename to tests/ember-data__json-api/tests/.gitkeep diff --git a/tests/ember-data__json-api/tests/index.html b/tests/ember-data__json-api/tests/index.html new file mode 100644 index 00000000000..e22ad2aa260 --- /dev/null +++ b/tests/ember-data__json-api/tests/index.html @@ -0,0 +1,35 @@ + + + + + + @ember-data/json-api Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+ + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + + diff --git a/tests/ember-data__json-api/tests/integration/-smoke.ts b/tests/ember-data__json-api/tests/integration/-smoke.ts new file mode 100644 index 00000000000..f07dede9c10 --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/-smoke.ts @@ -0,0 +1,7 @@ +import { module, test } from '@warp-drive/diagnostic'; + +module('Cache', function () { + test('Test Suit Configured', function (assert) { + assert.ok('We are configured'); + }); +}); diff --git a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts new file mode 100644 index 00000000000..d99aa8a37c1 --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts @@ -0,0 +1,367 @@ +import Cache from '@ember-data/json-api'; +import type { StructuredDataDocument } from '@ember-data/request'; +import type { NotificationType } from '@ember-data/store'; +import Store from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { CollectionResourceDocument, ResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; + +import { TestSchema } from '../../utils/schema'; + +function asStructuredDocument(doc: { + request?: { url: string; cacheOptions?: { key?: string } }; + content: T; +}): StructuredDataDocument { + return doc as unknown as StructuredDataDocument; +} + +type FakeRecord = { [key: string]: unknown; destroy: () => void }; + +class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + + override createCache(wrapper: CacheCapabilitiesManager) { + return new Cache(wrapper); + } + + override instantiateRecord(identifier: StableRecordIdentifier) { + const { id, lid, type } = identifier; + const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; + Object.assign(record, (this.cache.peek(identifier) as ResourceObject).attributes); + + const token = this.notifications.subscribe( + identifier, + (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { + if (kind === 'attributes' && key) { + record[key] = this.cache.getAttr(identifier, key); + } + } + ); + + record.destroy = () => { + this.notifications.unsubscribe(token); + }; + + return record; + } + + override teardownRecord(record: FakeRecord) { + record.destroy(); + } +} + +module('Integration | @ember-data/json-api Cache.put()', function () { + test('simple collection resource documents are correctly managed', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: [ + { type: 'user', id: '1', attributes: { name: 'Chris' } }, + { type: 'user', id: '2', attributes: { name: 'Wesley' } }, + ], + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + + assert.deepEqual(responseDocument.data, [identifier, identifier2], 'We were given the correct data back'); + }); + + test('collection resource documents are correctly cached', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + data: [ + { type: 'user', id: '1', attributes: { name: 'Chris' } }, + { type: 'user', id: '2', attributes: { name: 'Wesley' } }, + ], + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '2', + }) as StableExistingRecordIdentifier; + assert.equal(identifier.id, '1', 'We were given the correct data back'); + assert.equal(identifier2.id, '2', 'We were given the correct data back'); + + assert.deepEqual(responseDocument.data, [identifier, identifier2], 'We were given the correct data back'); + + const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + data: [identifier, identifier2], + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse, + { + lid: 'https://api.example.com/v1/users', + data: [identifier, identifier2], + }, + 'We got the cached response document back' + ); + }); + + test('resources are accessible via `peek`', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: [{ type: 'user', id: '1', attributes: { name: 'Chris' } }], + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + + assert.deepEqual(responseDocument.data, [identifier], 'We were given the correct data back'); + + let resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, + 'We can fetch from the cache' + ); + + const record = store.peekRecord<{ name: string | null }>(identifier); + + assert.equal(record?.name, 'Chris', 'record name is correct'); + + store.cache.setAttr(identifier, 'name', 'James'); + resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'James' }, relationships: {} }, + 'Resource Blob is kept updated in the cache after mutation' + ); + + store.cache.put( + asStructuredDocument({ + content: { + data: [{ type: 'user', id: '1', attributes: { username: '@runspired' } }], + }, + }) + ); + + resourceData = store.cache.peek(identifier); + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'James', username: '@runspired' }, + relationships: {}, + }, + 'Resource Blob is kept updated in the cache after additional put' + ); + + store.cache.rollbackAttrs(identifier); + resourceData = store.cache.peek(identifier); + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'Chris', username: '@runspired' }, + relationships: {}, + }, + 'Resource Blob is kept updated in the cache after rollback' + ); + }); + + test('resource relationships are accessible via `peek`', function (assert) { + const store = new TestStore(); + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'name', type: null }, + { + kind: 'belongsTo', + type: 'user', + name: 'bestFriend', + options: { + async: false, + inverse: 'bestFriend', + }, + }, + { + kind: 'belongsTo', + type: 'user', + name: 'worstEnemy', + options: { + async: false, + inverse: null, + }, + }, + { + kind: 'hasMany', + type: 'user', + name: 'friends', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); + + let responseDocument: CollectionResourceDataDocument; + store._run(() => { + responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: [ + { + type: 'user', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + worstEnemy: { + data: { type: 'user', id: '3' }, + }, + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + ], + included: [ + { + type: 'user', + id: '2', + attributes: { name: 'Wesley' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { name: 'Rey' }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + ], + }, + }) + ); + }); + const identifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + const identifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); + + assert.deepEqual(responseDocument!.data, [identifier1], 'We were given the correct data back'); + + const resourceData1 = store.cache.peek(identifier1); + const resourceData2 = store.cache.peek(identifier2); + const resourceData3 = store.cache.peek(identifier3); + + assert.deepEqual( + resourceData1, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: identifier2, + }, + friends: { + data: [identifier2, identifier3], + }, + worstEnemy: { + data: identifier3, + }, + }, + }, + 'We can fetch from the cache' + ); + assert.deepEqual( + resourceData2, + { + type: 'user', + id: '2', + lid: '@lid:user-2', + attributes: { name: 'Wesley' }, + relationships: { + bestFriend: { + data: identifier1, + }, + friends: { + data: [identifier1, identifier3], + }, + }, + }, + 'We can fetch included data from the cache' + ); + assert.deepEqual( + resourceData3, + { + type: 'user', + id: '3', + lid: '@lid:user-3', + attributes: { name: 'Rey' }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [identifier1, identifier2], + }, + }, + }, + 'We can fetch more included data from the cache' + ); + }); +}); diff --git a/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts new file mode 100644 index 00000000000..143e01776c5 --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts @@ -0,0 +1,107 @@ +import Cache from '@ember-data/json-api'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import { buildBaseURL } from '@ember-data/request-utils'; +import Store, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import { module, test } from '@warp-drive/diagnostic'; +import { mock, MockServerHandler } from '@warp-drive/holodeck'; + +const RECORD = false; + +function isNetworkError(e: unknown): asserts e is Error & { + status: number; + statusText: string; + code: number; + name: string; + isRequestError: boolean; + content?: object; + errors?: object[]; +} { + if (!(e instanceof Error)) { + throw new Error('Expected a network error'); + } +} + +class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + return new Cache(wrapper); + } +} + +module('Integration | @ember-data/json-api Cach.put()', function (hooks) { + test('Useful errors are propagated by the CacheHandler', async function (assert) { + const manager = new RequestManager(); + const store = new TestStore(); + + manager.use([new MockServerHandler(this), Fetch]); + manager.useCache(CacheHandler); + store.requestManager = manager; + + await mock( + this, + () => ({ + url: 'users/1', + status: 404, + headers: {}, + method: 'GET', + statusText: 'Not Found', + body: null, + response: { + errors: [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + }, + }), + RECORD + ); + + const url = buildBaseURL({ resourcePath: 'users/1' }); + try { + await store.request({ url }); + assert.ok(false, 'Should have thrown'); + } catch (e) { + isNetworkError(e); + assert.true(e instanceof AggregateError, 'The error is an AggregateError'); + assert.equal(e.message, `[404 Not Found] GET (cors) - ${url}`, 'The error message is correct'); + assert.equal(e.status, 404, 'The error status is correct'); + assert.equal(e.statusText, 'Not Found', 'The error statusText is correct'); + assert.equal(e.code, 404, 'The error code is correct'); + assert.equal(e.name, 'NotFoundError', 'The error code is correct'); + assert.true(e.isRequestError, 'The error is a request error'); + + // error.content is present + assert.satisfies( + // @ts-expect-error content property is loosely typed + e.content, + { + errors: [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + }, + 'The error.content is present' + ); + + // error.errors is present + assert.deepEqual( + e.errors, + [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + 'The error.errors is present' + ); + } + }); +}); diff --git a/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts new file mode 100644 index 00000000000..cf660bbd1bc --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts @@ -0,0 +1,334 @@ +import Cache from '@ember-data/json-api'; +import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; +import type { DocumentCacheOperation } from '@ember-data/store'; +import Store from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { StableDocumentIdentifier, StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { CollectionResourceDataDocument, ResourceMetaDocument } from '@warp-drive/core-types/spec/document'; +import { module, test } from '@warp-drive/diagnostic'; + +import { TestSchema } from '../../utils/schema'; + +function asStructuredDocument(doc: { + request?: { url: string; cacheOptions?: { key?: string } }; + content: T; +}): StructuredDataDocument { + return doc as unknown as StructuredDataDocument; +} + +class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + override createCache(wrapper: CacheCapabilitiesManager) { + return new Cache(wrapper); + } +} + +module('Integration | @ember-data/json-api Cach.put()', function (hooks) { + test('meta documents are correctly cached', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + meta: { count: 4 }, + }, + }) + ); + + assert.false('data' in responseDocument, 'No data is associated'); + assert.deepEqual(responseDocument.meta, { count: 4 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4 }), 'meta is correct'); + assert.equal(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); + + const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + meta: { count: 4 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse, + { + lid: 'https://api.example.com/v1/users', + meta: { count: 4 }, + }, + 'We got the cached response document back' + ); + }); + + test('meta documents respect cacheOptions.key', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users', cacheOptions: { key: 'users' } }, + content: { + meta: { count: 4 }, + }, + }) + ); + + assert.false('data' in responseDocument, 'No data is associated'); + assert.deepEqual(responseDocument.meta, { count: 4 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4 }), 'meta is correct'); + assert.equal(responseDocument.lid, 'users', 'lid is correct'); + + const structuredDocument = store.cache.peekRequest({ lid: 'users' }); + const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.equal(structuredDocument2, null, 'url is not cache key'); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users', cacheOptions: { key: 'users' } }, + content: { + lid: 'users', + meta: { count: 4 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'users' }); + const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.equal(cachedResponse2, null, 'url is not cache key'); + assert.deepEqual( + cachedResponse, + { + lid: 'users', + meta: { count: 4 }, + }, + 'We got the cached response document back' + ); + }); + + test('meta documents are correctly updated', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + meta: { count: 4, last: 4 }, + }, + }) + ); + + assert.false('data' in responseDocument, 'No data is associated'); + assert.deepEqual(responseDocument.meta, { count: 4, last: 4 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4, last: 4 }), 'meta is correct'); + assert.equal(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); + + const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + meta: { count: 4, last: 4 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse, + { + lid: 'https://api.example.com/v1/users', + meta: { count: 4, last: 4 }, + }, + 'We got the cached response document back' + ); + + const responseDocument2 = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + meta: { count: 3, next: 8 }, + }, + }) + ); + + assert.false('data' in responseDocument2, 'No data is associated'); + assert.deepEqual(responseDocument2.meta, { count: 3, next: 8 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument2.meta), JSON.stringify({ count: 3, next: 8 }), 'meta is correct'); + assert.equal(responseDocument2.lid, 'https://api.example.com/v1/users', 'lid is correct'); + + const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument2 as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + meta: { count: 3, next: 8 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse2, + { + lid: 'https://api.example.com/v1/users', + meta: { count: 3, next: 8 }, + }, + 'We got the cached response document back' + ); + }); + + test('updating cache with a meta document disregards prior data', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + data: [{ type: 'user', id: '1', attributes: { name: 'Chris' } }], + meta: { count: 4, last: 4 }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + + assert.deepEqual(responseDocument.data, [identifier], 'data is associated'); + assert.deepEqual(responseDocument.meta, { count: 4, last: 4 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4, last: 4 }), 'meta is correct'); + assert.equal(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); + + const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + data: [identifier], + meta: { count: 4, last: 4 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse, + { + lid: 'https://api.example.com/v1/users', + data: [identifier], + meta: { count: 4, last: 4 }, + }, + 'We got the cached response document back' + ); + + const responseDocument2 = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users' }, + content: { + meta: { count: 3, next: 8 }, + }, + }) + ); + + assert.false('data' in responseDocument2, 'No data is associated'); + assert.deepEqual(responseDocument2.meta, { count: 3, next: 8 }, 'meta is correct'); + assert.equal(JSON.stringify(responseDocument2.meta), JSON.stringify({ count: 3, next: 8 }), 'meta is correct'); + assert.equal(responseDocument2.lid, 'https://api.example.com/v1/users', 'lid is correct'); + + const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + structuredDocument2 as Partial>, + { + request: { url: 'https://api.example.com/v1/users' }, + content: { + lid: 'https://api.example.com/v1/users', + meta: { count: 3, next: 8 }, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); + assert.deepEqual( + cachedResponse2, + { + lid: 'https://api.example.com/v1/users', + meta: { count: 3, next: 8 }, + }, + 'We got the cached response document back' + ); + }); + + test("notifications are generated for create and update of the document's cache key", function (assert) { + assert.expect(10); + const store = new TestStore(); + const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ + url: '/api/v1/query?type=user&name=Chris&limit=1', + })!; + + let isUpdating = false; + store.notifications.subscribe('document', (identifier: StableDocumentIdentifier, type: DocumentCacheOperation) => { + if (isUpdating) { + assert.equal(type, 'updated', 'We were notified of an update'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } else { + assert.equal(type, 'added', 'We were notified of an add'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } + }); + + store.notifications.subscribe( + documentIdentifier, + (identifier: StableDocumentIdentifier, type: DocumentCacheOperation) => { + if (isUpdating) { + assert.equal(type, 'updated', 'We were notified of an update'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } else { + assert.equal(type, 'added', 'We were notified of an add'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } + } + ); + + store._run(() => { + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { + url: '/api/v1/query?type=user&name=Chris&limit=1', + }, + content: { + meta: { count: 4 }, + }, + }) + ); + + assert.equal(responseDocument.meta.count, 4, 'We were given the correct data back'); + }); + + isUpdating = true; + store._run(() => { + const responseDocument2 = store.cache.put( + asStructuredDocument({ + request: { + url: '/api/v1/query?type=user&name=Chris&limit=1', + }, + content: { + meta: { count: 3 }, + }, + }) + ); + + assert.equal(responseDocument2.meta.count, 3, 'We were given the correct data back'); + }); + }); +}); diff --git a/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts new file mode 100644 index 00000000000..7736497bac9 --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts @@ -0,0 +1,544 @@ +import Cache from '@ember-data/json-api'; +import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; +import type { DocumentCacheOperation, NotificationType } from '@ember-data/store'; +import Store from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; + +import { TestSchema } from '../../utils/schema'; + +type FakeRecord = { [key: string]: unknown; destroy: () => void }; + +class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + + override createCache(wrapper: CacheCapabilitiesManager) { + return new Cache(wrapper); + } + + override instantiateRecord(identifier: StableRecordIdentifier) { + const { id, lid, type } = identifier; + const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; + Object.assign(record, this.cache.peek(identifier)!.attributes); + + const token = this.notifications.subscribe( + identifier, + (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { + if (kind === 'attributes' && key) { + record[key] = this.cache.getAttr(identifier, key); + } + } + ); + + record.destroy = () => { + this.notifications.unsubscribe(token); + }; + + return record; + } + + override teardownRecord(record: FakeRecord) { + record.destroy(); + } +} + +function asStructuredDocument(doc: { + request?: { url: string; cacheOptions?: { key?: string } }; + content: T; +}): StructuredDataDocument { + return doc as unknown as StructuredDataDocument; +} + +module('Integration | @ember-data/json-api Cache.put()', function (hooks) { + test('simple single resource documents are correctly managed', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + + assert.equal(responseDocument.data, identifier, 'We were given the correct data back'); + }); + + test('single resource documents are correctly cached', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users/1' }, + content: { + data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + + assert.equal(responseDocument.data, identifier, 'We were given the correct data back'); + + const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users/1' }); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users/1' }, + content: { + lid: 'https://api.example.com/v1/users/1', + data: identifier, + }, + }, + 'We got the cached structured document back' + ); + const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users/1' }); + assert.deepEqual( + cachedResponse, + { + lid: 'https://api.example.com/v1/users/1', + data: identifier, + }, + 'We got the cached response document back' + ); + }); + + test('data documents respect cacheOptions.key', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { url: 'https://api.example.com/v1/users/1', cacheOptions: { key: 'user-1' } }, + content: { + data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + + assert.equal(responseDocument.data, identifier, 'We were given the correct data back'); + + const structuredDocument = store.cache.peekRequest({ lid: 'user-1' }); + const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users/1' }); + assert.equal(structuredDocument2, null, 'we did not use the url as the key'); + assert.deepEqual( + structuredDocument as Partial>, + { + request: { url: 'https://api.example.com/v1/users/1', cacheOptions: { key: 'user-1' } }, + content: { + lid: 'user-1', + data: identifier, + }, + }, + 'We got the cached structured document back' + ); + + const cachedResponse = store.cache.peek({ lid: 'user-1' }); + const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users/1' }); + assert.equal(cachedResponse2, null, 'we did not use the url as the key'); + assert.deepEqual( + cachedResponse, + { + lid: 'user-1', + data: identifier, + }, + 'We got the cached response document back' + ); + }); + + test("notifications are generated for create and update of the document's cache key", function (assert) { + assert.expect(10); + const store = new TestStore(); + const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ + url: '/api/v1/query?type=user&name=Chris&limit=1', + })!; + + let isUpdating = false; + store.notifications.subscribe('document', (identifier: StableDocumentIdentifier, type: DocumentCacheOperation) => { + if (isUpdating) { + assert.equal(type, 'updated', 'We were notified of an update'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } else { + assert.equal(type, 'added', 'We were notified of an add'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } + }); + + store.notifications.subscribe( + documentIdentifier, + (identifier: StableDocumentIdentifier, type: DocumentCacheOperation) => { + if (isUpdating) { + assert.equal(type, 'updated', 'We were notified of an update'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } else { + assert.equal(type, 'added', 'We were notified of an add'); + assert.equal(identifier, documentIdentifier, 'We were notified of the correct document'); + } + } + ); + + store._run(() => { + const responseDocument = store.cache.put( + asStructuredDocument({ + request: { + url: '/api/v1/query?type=user&name=Chris&limit=1', + }, + content: { + data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + + assert.equal(responseDocument.data, identifier, 'We were given the correct data back'); + }); + + isUpdating = true; + store._run(() => { + const responseDocument2 = store.cache.put( + asStructuredDocument({ + request: { + url: '/api/v1/query?type=user&name=Chris&limit=1', + }, + content: { + data: { type: 'user', id: '2', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + assert.equal(responseDocument2.data, identifier2, 'We were given the correct data back'); + }); + }); + + test('resources are accessible via `peek`', function (assert) { + const store = new TestStore(); + + const responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, + }, + }) + ); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + + assert.equal(responseDocument.data, identifier, 'We were given the correct data back'); + + let resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, + 'We can fetch from the cache' + ); + + const record = store.peekRecord<{ name: string | null }>(identifier); + + assert.equal(record?.name, 'Chris', 'record name is correct'); + + store.cache.setAttr(identifier, 'name', 'James'); + resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'James' }, relationships: {} }, + 'Resource Blob is kept updated in the cache after mutation' + ); + + store.cache.put( + asStructuredDocument({ + content: { + data: { type: 'user', id: '1', attributes: { username: '@runspired' } }, + }, + }) + ); + + resourceData = store.cache.peek(identifier); + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'James', username: '@runspired' }, + relationships: {}, + }, + 'Resource Blob is kept updated in the cache after additional put' + ); + + store.cache.rollbackAttrs(identifier); + resourceData = store.cache.peek(identifier); + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'Chris', username: '@runspired' }, + relationships: {}, + }, + 'Resource Blob is kept updated in the cache after rollback' + ); + }); + + test('single resource relationships are accessible via `peek`', function (assert) { + const store = new TestStore(); + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { name: 'name', kind: 'attribute', type: null }, + { + name: 'bestFriend', + kind: 'belongsTo', + type: 'user', + options: { + async: false, + inverse: 'bestFriend', + }, + }, + { + name: 'worstEnemy', + kind: 'belongsTo', + type: 'user', + options: { + async: false, + inverse: null, + }, + }, + { + name: 'friends', + kind: 'hasMany', + type: 'user', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); + + let responseDocument: SingleResourceDataDocument; + store._run(() => { + responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: { + type: 'user', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + worstEnemy: { + data: { type: 'user', id: '3' }, + }, + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { name: 'Wesley' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { name: 'Rey' }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + ], + }, + }) + ); + }); + const identifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + const identifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); + + assert.equal(responseDocument!.data, identifier1, 'We were given the correct data back'); + + const resourceData1 = store.cache.peek(identifier1); + const resourceData2 = store.cache.peek(identifier2); + const resourceData3 = store.cache.peek(identifier3); + + assert.deepEqual( + resourceData1, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: identifier2, + }, + friends: { + data: [identifier2, identifier3], + }, + worstEnemy: { + data: identifier3, + }, + }, + }, + 'We can fetch from the cache' + ); + assert.deepEqual( + resourceData2, + { + type: 'user', + id: '2', + lid: '@lid:user-2', + attributes: { name: 'Wesley' }, + relationships: { + bestFriend: { + data: identifier1, + }, + friends: { + data: [identifier1, identifier3], + }, + }, + }, + 'We can fetch included data from the cache' + ); + assert.deepEqual( + resourceData3, + { + type: 'user', + id: '3', + lid: '@lid:user-3', + attributes: { name: 'Rey' }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [identifier1, identifier2], + }, + }, + }, + 'We can fetch more included data from the cache' + ); + }); + + test('generated default values are retained', function (assert) { + const store = new TestStore(); + let i = 0; + + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + type: null, + options: { + // @ts-expect-error functions are not allowed in schema + defaultValue: () => { + i++; + return `Name ${i}`; + }, + }, + }, + ], + }); + + store._run(() => { + store.cache.put( + asStructuredDocument({ + content: { + data: { + type: 'user', + id: '1', + attributes: {}, + }, + }, + }) + ); + }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + + const name1 = store.cache.getAttr(identifier, 'name'); + assert.equal(name1, 'Name 1', 'The default value was generated'); + const name2 = store.cache.getAttr(identifier, 'name'); + assert.equal(name2, 'Name 1', 'The default value was cached'); + + store.cache.setAttr(identifier, 'name', 'Chris'); + const name3 = store.cache.getAttr(identifier, 'name'); + assert.equal(name3, 'Chris', 'The value was updated'); + + store.cache.setAttr(identifier, 'name', null); + const name4 = store.cache.getAttr(identifier, 'name'); + assert.equal(name4, null, 'Null was set and maintained'); + + store.cache.rollbackAttrs(identifier); + const name5 = store.cache.getAttr(identifier, 'name'); + assert.equal(name5, 'Name 2', 'The default value was regenerated'); + + store._run(() => { + store.cache.put( + asStructuredDocument({ + content: { + data: { + type: 'user', + id: '1', + attributes: { + name: 'Tomster', + }, + }, + }, + }) + ); + }); + + const name6 = store.cache.getAttr(identifier, 'name'); + assert.equal(name6, 'Tomster', 'The value was updated on put'); + }); +}); diff --git a/tests/ember-data__json-api/tests/integration/serialize-test.ts b/tests/ember-data__json-api/tests/integration/serialize-test.ts new file mode 100644 index 00000000000..b1eefbaebcc --- /dev/null +++ b/tests/ember-data__json-api/tests/integration/serialize-test.ts @@ -0,0 +1,405 @@ +import Cache from '@ember-data/json-api'; +import { serializePatch, serializeResources } from '@ember-data/json-api/request'; +import type { NotificationType } from '@ember-data/store'; +import Store from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { ResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; +import { module, test } from '@warp-drive/diagnostic'; + +import { TestSchema } from '../utils/schema'; + +type FakeRecord = { [key: string]: unknown; destroy: () => void }; +class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + override createCache(wrapper: CacheCapabilitiesManager) { + return new Cache(wrapper); + } + + override instantiateRecord(identifier: StableRecordIdentifier) { + const { id, lid, type } = identifier; + const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; + Object.assign(record, (this.cache.peek(identifier) as ResourceObject).attributes); + + const token = this.notifications.subscribe( + identifier, + (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { + if (kind === 'attributes' && key) { + record[key] = this.cache.getAttr(identifier, key); + } + } + ); + + record.destroy = () => { + this.notifications.unsubscribe(token); + }; + + return record; + } + + override teardownRecord(record: FakeRecord) { + record.destroy(); + } +} + +module('Integration | @ember-data/json-api/request', function (hooks) { + let store: TestStore; + hooks.beforeEach(function () { + store = new TestStore(); + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'firstName', type: null }, + { kind: 'attribute', name: 'lastName', type: null }, + { + kind: 'belongsTo', + type: 'user', + name: 'bestFriend', + options: { + async: false, + inverse: 'bestFriend', + }, + }, + { + kind: 'belongsTo', + type: 'user', + name: 'worstEnemy', + options: { + async: false, + inverse: null, + }, + }, + { + kind: 'hasMany', + type: 'user', + name: 'friends', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); + + store.push({ + data: { + type: 'user', + id: '1', + attributes: { firstName: 'Chris', lastName: 'Thoburn' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + worstEnemy: { + data: { type: 'user', id: '3' }, + }, + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { firstName: 'Wesley', lastName: 'Thoburn' }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '3' }, + ], + }, + }, + }, + { + type: 'user', + id: '3', + attributes: { firstName: 'Rey', lastName: 'Skybarker' }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [ + { type: 'user', id: '1' }, + { type: 'user', id: '2' }, + ], + }, + }, + }, + ], + }); + }); + + module('serializePatch', function () { + test('Correctly serializes only changed attributes and relationships', function (assert) { + const user1Identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + store.cache.setAttr(user1Identifier, 'firstName', 'Christopher'); + + let patch = serializePatch(store.cache, user1Identifier); + assert.deepEqual( + patch, + { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + firstName: 'Christopher', + }, + }, + }, + 'Correctly serializes changed attributes' + ); + + // set the attr back to initial state to remove it from diff + store.cache.setAttr(user1Identifier, 'firstName', 'Chris'); + + store._join(() => { + // change a belongsTo relationship + store.cache.mutate({ + op: 'replaceRelatedRecord', + record: user1Identifier, + field: 'bestFriend', + value: null, + }); + }); + + patch = serializePatch(store.cache, user1Identifier); + assert.equal(patch.data.attributes, undefined, 'Correctly serializes changed attributes when there are none'); + assert.deepEqual( + patch, + { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + relationships: { + bestFriend: { + data: null, + }, + }, + }, + }, + 'Correctly serializes changed belongsTo relationships' + ); + store.cache.rollbackRelationships(user1Identifier); + + store._join(() => { + // change a hasMany relationship + store.cache.mutate({ + op: 'addToRelatedRecords', + record: user1Identifier, + field: 'friends', + value: store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '4' }), + }); + }); + + patch = serializePatch(store.cache, user1Identifier); + assert.equal(patch.data.relationships?.bestFriend, undefined, 'Correctly serializes rolled back relationships'); + assert.deepEqual( + patch, + { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2', lid: '@lid:user-2' } as StableRecordIdentifier, + { type: 'user', id: '3', lid: '@lid:user-3' } as StableRecordIdentifier, + { type: 'user', id: '4', lid: '@lid:user-4' } as StableRecordIdentifier, + ], + }, + }, + }, + }, + 'Correctly serializes changed hasMany relationships' + ); + + store._join(() => { + // change a hasMany relationship + store.cache.mutate({ + op: 'removeFromRelatedRecords', + record: user1Identifier, + field: 'friends', + value: store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }), + }); + }); + + patch = serializePatch(store.cache, user1Identifier); + assert.deepEqual( + patch, + { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + relationships: { + friends: { + data: [ + { type: 'user', id: '3', lid: '@lid:user-3' } as StableRecordIdentifier, + { type: 'user', id: '4', lid: '@lid:user-4' } as StableRecordIdentifier, + ], + }, + }, + }, + }, + 'Correctly serializes changed hasMany relationships' + ); + + store._join(() => { + // change a hasMany relationship + store.cache.mutate({ + op: 'replaceRelatedRecords', + record: user1Identifier, + field: 'friends', + value: [ + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }), + ], + }); + }); + + patch = serializePatch(store.cache, user1Identifier); + assert.deepEqual( + patch, + { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + relationships: { + friends: { + data: [ + { type: 'user', id: '3', lid: '@lid:user-3' } as StableRecordIdentifier, + { type: 'user', id: '2', lid: '@lid:user-2' } as StableRecordIdentifier, + ], + }, + }, + }, + }, + 'Correctly serializes changed hasMany relationships' + ); + }); + }); + + module('serializeResources', function () { + test('Correctly serializes single resources', function (assert) { + const payload = serializeResources( + store.cache, + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }) + ); + assert.deepEqual(payload, { + data: { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2', lid: '@lid:user-2' }, + }, + worstEnemy: { + data: { type: 'user', id: '3', lid: '@lid:user-3' }, + }, + friends: { + data: [ + { type: 'user', id: '2', lid: '@lid:user-2' }, + { type: 'user', id: '3', lid: '@lid:user-3' }, + ], + }, + }, + }, + }); + }); + test('Correctly serializes multiple resources', function (assert) { + const payload = serializeResources(store.cache, [ + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }), + store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }), + ]); + assert.deepEqual(payload, { + data: [ + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + firstName: 'Chris', + lastName: 'Thoburn', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2', lid: '@lid:user-2' }, + }, + worstEnemy: { + data: { type: 'user', id: '3', lid: '@lid:user-3' }, + }, + friends: { + data: [ + { type: 'user', id: '2', lid: '@lid:user-2' }, + { type: 'user', id: '3', lid: '@lid:user-3' }, + ], + }, + }, + }, + { + type: 'user', + id: '2', + lid: '@lid:user-2', + attributes: { + firstName: 'Wesley', + lastName: 'Thoburn', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1', lid: '@lid:user-1' }, + }, + friends: { + data: [ + { type: 'user', id: '1', lid: '@lid:user-1' }, + { type: 'user', id: '3', lid: '@lid:user-3' }, + ], + }, + }, + }, + { + type: 'user', + id: '3', + lid: '@lid:user-3', + attributes: { + firstName: 'Rey', + lastName: 'Skybarker', + }, + relationships: { + bestFriend: { + data: null, + }, + friends: { + data: [ + { type: 'user', id: '1', lid: '@lid:user-1' }, + { type: 'user', id: '2', lid: '@lid:user-2' }, + ], + }, + }, + }, + ], + }); + }); + }); +}); diff --git a/tests/ember-data__json-api/tests/test-helper.js b/tests/ember-data__json-api/tests/test-helper.js new file mode 100644 index 00000000000..9cd875e8169 --- /dev/null +++ b/tests/ember-data__json-api/tests/test-helper.js @@ -0,0 +1,31 @@ +import { setBuildURLConfig } from '@ember-data/request-utils'; +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; +import { setConfig, setTestId } from '@warp-drive/holodeck'; + +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; +setBuildURLConfig({ + host: MockHost, + namespace: '', +}); +setConfig({ host: MockHost }); +setupGlobalHooks((hooks) => { + hooks.beforeEach(function (assert) { + setTestId(this, assert.test.testId); + }); + hooks.afterEach(function () { + setTestId(this, null); + }); +}); + +configure(); + +start({ + tryCatch: false, + debug: false, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/ember-data__json-api/tests/utils/schema.ts b/tests/ember-data__json-api/tests/utils/schema.ts new file mode 100644 index 00000000000..80b0ef45933 --- /dev/null +++ b/tests/ember-data__json-api/tests/utils/schema.ts @@ -0,0 +1,170 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + LegacyAttributeField, + LegacyRelationshipSchema, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + } + hasTrait(type: string): boolean { + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `'${kind}' fields cannot be transformed. Only fields of kind 'field' 'object' or 'array' can specify a transformation. Attempted to find '${field.type ?? ''}' on field '${name}'.`, + !('kind' in field) || ['field', 'object', 'array'].includes(kind) + ); + assert( + `Expected the '${kind}' field '${name}' to specify a transformation via 'field.type', but none was present`, + field.type + ); + assert( + `No transformation registered with name '${field.type}' for '${kind}' field '${name}'`, + this._transforms.has(field.type) + ); + return this._transforms.get(field.type)!; + } + derivation(field: DerivedField | { type: string }): Derivation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not derived and so cannot be used to lookup a derivation`, + !('kind' in field) || kind === 'derived' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a derivation via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' derivation registered for use by the '${kind}' field '${name}'`, + this._derivations.has(field.type) + ); + return this._derivations.get(field.type)!; + } + hashFn(field: HashField | { type: string }): HashFn { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not a HashField and so cannot be used to lookup a hash function`, + !('kind' in field) || kind === '@hash' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a hash function via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' hash function is registered for use by the '${kind}' field '${name}'`, + this._hashFns.has(field.type) + ); + return this._hashFns.get(field.type)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + + registerTransformation(transformation: Transformation): void { + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/ember-data__json-api/tsconfig.json b/tests/ember-data__json-api/tsconfig.json new file mode 100644 index 00000000000..1b019f68942 --- /dev/null +++ b/tests/ember-data__json-api/tsconfig.json @@ -0,0 +1,98 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@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/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"], + "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types"], + "@warp-drive/holodeck/*": ["../../packages/holodeck/unstable-preview-types/*"], + "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/diagnostic" + }, + { + "path": "../../packages/holodeck" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/ember-data__model/README.md b/tests/ember-data__model/README.md new file mode 100644 index 00000000000..a1daf062ec9 --- /dev/null +++ b/tests/ember-data__model/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/model + +This test-package provides tests for the `@ember-data/model` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__model; +pnpm test; +``` diff --git a/tests/debug-encapsulation/app/app.js b/tests/ember-data__model/app/app.js similarity index 100% rename from tests/debug-encapsulation/app/app.js rename to tests/ember-data__model/app/app.js diff --git a/tests/ember-data__model/app/index.html b/tests/ember-data__model/app/index.html new file mode 100644 index 00000000000..1c50668cf79 --- /dev/null +++ b/tests/ember-data__model/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/model + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/adapter-encapsulation/app/resolver.js b/tests/ember-data__model/app/resolver.js similarity index 100% rename from tests/adapter-encapsulation/app/resolver.js rename to tests/ember-data__model/app/resolver.js diff --git a/tests/debug-encapsulation/app/router.js b/tests/ember-data__model/app/router.js similarity index 100% rename from tests/debug-encapsulation/app/router.js rename to tests/ember-data__model/app/router.js diff --git a/tests/json-api/app/styles/app.css b/tests/ember-data__model/app/styles/app.css similarity index 100% rename from tests/json-api/app/styles/app.css rename to tests/ember-data__model/app/styles/app.css diff --git a/tests/model-encapsulation/app/templates/application.hbs b/tests/ember-data__model/app/templates/application.hbs similarity index 100% rename from tests/model-encapsulation/app/templates/application.hbs rename to tests/ember-data__model/app/templates/application.hbs diff --git a/tests/ember-data__model/config/environment.js b/tests/ember-data__model/config/environment.js new file mode 100644 index 00000000000..8110ff7112a --- /dev/null +++ b/tests/ember-data__model/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__model', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/json-api-encapsulation/config/optional-features.json b/tests/ember-data__model/config/optional-features.json similarity index 100% rename from tests/json-api-encapsulation/config/optional-features.json rename to tests/ember-data__model/config/optional-features.json diff --git a/tests/json-api-encapsulation/config/targets.js b/tests/ember-data__model/config/targets.js similarity index 100% rename from tests/json-api-encapsulation/config/targets.js rename to tests/ember-data__model/config/targets.js diff --git a/tests/ember-data__model/diagnostic.js b/tests/ember-data__model/diagnostic.js new file mode 100644 index 00000000000..ede75dbb1ad --- /dev/null +++ b/tests/ember-data__model/diagnostic.js @@ -0,0 +1,3 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; + +await launch(); diff --git a/tests/ember-data__model/ember-cli-build.js b/tests/ember-data__model/ember-cli-build.js new file mode 100644 index 00000000000..436ede741f6 --- /dev/null +++ b/tests/ember-data__model/ember-cli-build.js @@ -0,0 +1,28 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); + + return app.toTree(); +}; diff --git a/tests/ember-data__model/eslint.config.mjs b/tests/ember-data__model/eslint.config.mjs new file mode 100644 index 00000000000..edabe45e43a --- /dev/null +++ b/tests/ember-data__model/eslint.config.mjs @@ -0,0 +1,36 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + diagnostic.browser({ + allowedImports: ['@ember/object'], + }), +]; diff --git a/tests/ember-data__model/package.json b/tests/ember-data__model/package.json new file mode 100644 index 00000000000..5f920110d7a --- /dev/null +++ b/tests/ember-data__model/package.json @@ -0,0 +1,113 @@ +{ + "name": "ember-data__model", + "version": "4.12.8", + "private": true, + "description": "Tests for @ember-data/model", + "repository": { + "type": "git", + "url": "https://github.com/emberjs/data.git", + "directory": "tests/ember-data__model" + }, + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "check:types": "tsc --noEmit", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "test": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@ember-data/adapter": { + "injected": true + }, + "@ember-data/serializer": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/diagnostic": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/adapter": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", + "@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.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-test-loader": "^3.1.0", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "loader.js": "^4.7.0", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "engines": { + "node": ">= 18.20.4" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } +} diff --git a/tests/ember-data__model/tests/index.html b/tests/ember-data__model/tests/index.html new file mode 100644 index 00000000000..d5e2fc68a64 --- /dev/null +++ b/tests/ember-data__model/tests/index.html @@ -0,0 +1,37 @@ + + + + + + @ember-data/model Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + +
+
+
+
+ + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/tests/ember-data__model/tests/integration/model-for-test.ts b/tests/ember-data__model/tests/integration/model-for-test.ts new file mode 100644 index 00000000000..e1fc5e4af4b --- /dev/null +++ b/tests/ember-data__model/tests/integration/model-for-test.ts @@ -0,0 +1,89 @@ +import Store from '@ember-data/store'; +import type { SchemaService } from '@ember-data/store/types'; +import { module, test } from '@warp-drive/diagnostic'; + +import { TestSchema } from '../utils/schema'; + +module('modelFor without @ember-data/model', function () { + test('We can call modelFor', function (assert) { + class TestStore extends Store { + createSchemaService(): SchemaService { + return new TestSchema(); + } + override instantiateRecord() { + return { + id: '1', + type: 'user', + name: 'Chris Thoburn', + }; + } + override teardownRecord() { + return; + } + } + const store = new TestStore(); + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }); + + try { + store.modelFor('user'); + assert.ok(true, 'We should not throw an eror when schema is available'); + } catch (e) { + assert.ok(false, `We threw an unexpected error when schema is available: ${(e as Error).message}`); + } + + try { + store.modelFor('person'); + assert.ok(false, 'We should throw an eror when no schema is available'); + } catch (e) { + assert.equal( + (e as Error).message, + "No model was found for 'person' and no schema handles the type", + 'We throw an error when no schema is available' + ); + } + }); + + test('modelFor returns a stable reference', function (assert) { + class TestStore extends Store { + createSchemaService(): SchemaService { + return new TestSchema(); + } + override instantiateRecord() { + return { + id: '1', + type: 'user', + name: 'Chris Thoburn', + }; + } + override teardownRecord() { + return; + } + } + const store = new TestStore(); + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }); + + const ShimUser1 = store.modelFor('user'); + const ShimUser2 = store.modelFor('user'); + assert.equal(ShimUser1, ShimUser2, 'Repeat modelFor calls return the same shim'); + }); +}); diff --git a/tests/ember-data__model/tests/test-helper.js b/tests/ember-data__model/tests/test-helper.js new file mode 100644 index 00000000000..bcb6a580430 --- /dev/null +++ b/tests/ember-data__model/tests/test-helper.js @@ -0,0 +1,12 @@ +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; + +configure(); + +start({ + tryCatch: false, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/ember-data__model/tests/utils/schema.ts b/tests/ember-data__model/tests/utils/schema.ts new file mode 100644 index 00000000000..80b0ef45933 --- /dev/null +++ b/tests/ember-data__model/tests/utils/schema.ts @@ -0,0 +1,170 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + LegacyAttributeField, + LegacyRelationshipSchema, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + } + hasTrait(type: string): boolean { + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `'${kind}' fields cannot be transformed. Only fields of kind 'field' 'object' or 'array' can specify a transformation. Attempted to find '${field.type ?? ''}' on field '${name}'.`, + !('kind' in field) || ['field', 'object', 'array'].includes(kind) + ); + assert( + `Expected the '${kind}' field '${name}' to specify a transformation via 'field.type', but none was present`, + field.type + ); + assert( + `No transformation registered with name '${field.type}' for '${kind}' field '${name}'`, + this._transforms.has(field.type) + ); + return this._transforms.get(field.type)!; + } + derivation(field: DerivedField | { type: string }): Derivation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not derived and so cannot be used to lookup a derivation`, + !('kind' in field) || kind === 'derived' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a derivation via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' derivation registered for use by the '${kind}' field '${name}'`, + this._derivations.has(field.type) + ); + return this._derivations.get(field.type)!; + } + hashFn(field: HashField | { type: string }): HashFn { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not a HashField and so cannot be used to lookup a hash function`, + !('kind' in field) || kind === '@hash' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a hash function via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' hash function is registered for use by the '${kind}' field '${name}'`, + this._hashFns.has(field.type) + ); + return this._hashFns.get(field.type)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + + registerTransformation(transformation: Transformation): void { + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/ember-data__model/tsconfig.json b/tests/ember-data__model/tsconfig.json new file mode 100644 index 00000000000..079ccd949f1 --- /dev/null +++ b/tests/ember-data__model/tsconfig.json @@ -0,0 +1,89 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/adapter": ["../../packages/adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../../packages/adapter/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../../packages/serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../../packages/serializer/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/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/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"], + "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/adapter" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/serializer" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/diagnostic" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.body.br b/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.body.br new file mode 100644 index 00000000000..8332a7f4e94 Binary files /dev/null and b/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.body.br differ diff --git a/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.meta.json b/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.meta.json new file mode 100644 index 00000000000..080c75c05cf --- /dev/null +++ b/tests/ember-data__request/.mock-cache/409aab98/GET-0-users/1/res.meta.json @@ -0,0 +1,12 @@ +{ + "url": "users/1", + "status": 404, + "statusText": "Not Found", + "headers": { + "Content-Type": "application/vnd.api+json", + "Content-Encoding": "br", + "Cache-Control": "no-store" + }, + "method": "GET", + "requestBody": null +} \ No newline at end of file diff --git a/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.body.br b/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.body.br new file mode 100644 index 00000000000..95a61de6c7d Binary files /dev/null and b/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.body.br differ diff --git a/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.meta.json b/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.meta.json new file mode 100644 index 00000000000..82a5960c1e6 --- /dev/null +++ b/tests/ember-data__request/.mock-cache/b111b348/GET-0-users/1/res.meta.json @@ -0,0 +1,12 @@ +{ + "url": "users/1", + "status": 200, + "statusText": "OK", + "headers": { + "Content-Type": "application/vnd.api+json", + "Content-Encoding": "br", + "Cache-Control": "no-store" + }, + "method": "GET", + "requestBody": null +} \ No newline at end of file diff --git a/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.body.br b/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.body.br new file mode 100644 index 00000000000..95a61de6c7d Binary files /dev/null and b/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.body.br differ diff --git a/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.meta.json b/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.meta.json new file mode 100644 index 00000000000..82a5960c1e6 --- /dev/null +++ b/tests/ember-data__request/.mock-cache/c6767456/GET-0-users/1/res.meta.json @@ -0,0 +1,12 @@ +{ + "url": "users/1", + "status": 200, + "statusText": "OK", + "headers": { + "Content-Type": "application/vnd.api+json", + "Content-Encoding": "br", + "Cache-Control": "no-store" + }, + "method": "GET", + "requestBody": null +} \ No newline at end of file diff --git a/tests/ember-data__request/README.md b/tests/ember-data__request/README.md new file mode 100644 index 00000000000..e8330928343 --- /dev/null +++ b/tests/ember-data__request/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/request + +This test-package provides tests for the `@ember-data/request` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__request; +pnpm test; +``` diff --git a/tests/ember-data__request/app/app.ts b/tests/ember-data__request/app/app.ts new file mode 100644 index 00000000000..8f235d166e6 --- /dev/null +++ b/tests/ember-data__request/app/app.ts @@ -0,0 +1,16 @@ +import Application from '@ember/application'; + +import loadInitializers from 'ember-load-initializers'; + +import config from './config/environment'; +import Resolver from './resolver'; + +class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + override Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/tests/request/app/config/environment.d.ts b/tests/ember-data__request/app/config/environment.d.ts similarity index 100% rename from tests/request/app/config/environment.d.ts rename to tests/ember-data__request/app/config/environment.d.ts diff --git a/tests/ember-data__request/app/index.html b/tests/ember-data__request/app/index.html new file mode 100644 index 00000000000..234a83a0f2a --- /dev/null +++ b/tests/ember-data__request/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/request Tests + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/debug-encapsulation/app/resolver.js b/tests/ember-data__request/app/resolver.ts similarity index 100% rename from tests/debug-encapsulation/app/resolver.js rename to tests/ember-data__request/app/resolver.ts diff --git a/tests/json-api-encapsulation/app/router.js b/tests/ember-data__request/app/router.ts similarity index 100% rename from tests/json-api-encapsulation/app/router.js rename to tests/ember-data__request/app/router.ts diff --git a/tests/model-encapsulation/app/styles/app.css b/tests/ember-data__request/app/styles/app.css similarity index 100% rename from tests/model-encapsulation/app/styles/app.css rename to tests/ember-data__request/app/styles/app.css diff --git a/tests/adapter-encapsulation/tests/helpers/.gitkeep b/tests/ember-data__request/app/templates/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/tests/helpers/.gitkeep rename to tests/ember-data__request/app/templates/.gitkeep diff --git a/tests/ember-data__request/app/templates/application.hbs b/tests/ember-data__request/app/templates/application.hbs new file mode 100644 index 00000000000..e2147cab02d --- /dev/null +++ b/tests/ember-data__request/app/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/tests/ember-data__request/config/environment.js b/tests/ember-data__request/config/environment.js new file mode 100644 index 00000000000..98be18b2f4f --- /dev/null +++ b/tests/ember-data__request/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__request', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/json-api/config/optional-features.json b/tests/ember-data__request/config/optional-features.json similarity index 100% rename from tests/json-api/config/optional-features.json rename to tests/ember-data__request/config/optional-features.json diff --git a/tests/json-api/config/targets.js b/tests/ember-data__request/config/targets.js similarity index 100% rename from tests/json-api/config/targets.js rename to tests/ember-data__request/config/targets.js diff --git a/tests/ember-data__request/diagnostic.js b/tests/ember-data__request/diagnostic.js new file mode 100644 index 00000000000..3d888b65b69 --- /dev/null +++ b/tests/ember-data__request/diagnostic.js @@ -0,0 +1,13 @@ +import launch from '@warp-drive/diagnostic/server/default-setup.js'; +import holodeck from '@warp-drive/holodeck'; + +await launch({ + async setup(info) { + await holodeck.launchProgram({ + port: info.port + 1, + }); + }, + async cleanup() { + await holodeck.endProgram(); + }, +}); diff --git a/tests/ember-data__request/ember-cli-build.js b/tests/ember-data__request/ember-cli-build.js new file mode 100644 index 00000000000..436ede741f6 --- /dev/null +++ b/tests/ember-data__request/ember-cli-build.js @@ -0,0 +1,28 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); + + return app.toTree(); +}; diff --git a/tests/ember-data__request/eslint.config.mjs b/tests/ember-data__request/eslint.config.mjs new file mode 100644 index 00000000000..0713926424c --- /dev/null +++ b/tests/ember-data__request/eslint.config.mjs @@ -0,0 +1,36 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as diagnostic from '@warp-drive/internal-config/eslint/diagnostic.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + diagnostic.browser({ + allowedImports: ['@ember/application', '@ember/owner', '@ember/service'], + }), +]; diff --git a/tests/ember-data__request/package.json b/tests/ember-data__request/package.json new file mode 100644 index 00000000000..e370f306d17 --- /dev/null +++ b/tests/ember-data__request/package.json @@ -0,0 +1,99 @@ +{ + "name": "ember-data__request", + "version": "4.12.8", + "private": true, + "description": "Provides tests for @ember-data/request", + "keywords": [], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com:emberjs/data.git", + "directory": "tests/ember-data__request" + }, + "license": "MIT", + "author": "", + "directories": { + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "start": "bun run build:tests --watch", + "_build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "test": "bun ./diagnostic.js", + "_test:production": "bun ./diagnostic.js", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@warp-drive/diagnostic": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@warp-drive/holodeck": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember/edition-utils": "^1.2.0", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.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/diagnostic": "workspace:*", + "@warp-drive/holodeck": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "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", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "~4.0.2", + "ember-cli-test-loader": "^3.1.0", + "ember-disable-prototype-extensions": "^1.1.3", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "ember-source-channel-url": "^3.0.0", + "ember-try": "^3.0.0", + "loader.js": "^4.7.0", + "silent-error": "^1.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "ember": { + "edition": "octane" + }, + "engines": { + "node": ">= 18.20.4" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } +} diff --git a/tests/request/public/assets/demo-fetch.json b/tests/ember-data__request/public/assets/demo-fetch.json similarity index 100% rename from tests/request/public/assets/demo-fetch.json rename to tests/ember-data__request/public/assets/demo-fetch.json diff --git a/tests/adapter-encapsulation/tests/integration/.gitkeep b/tests/ember-data__request/tests/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/tests/integration/.gitkeep rename to tests/ember-data__request/tests/.gitkeep diff --git a/tests/ember-data__request/tests/index.html b/tests/ember-data__request/tests/index.html new file mode 100644 index 00000000000..bb9ca9edb6e --- /dev/null +++ b/tests/ember-data__request/tests/index.html @@ -0,0 +1,37 @@ + + + + + + @ember-data/request Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+ + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + + diff --git a/tests/request/tests/integration/abort-test.ts b/tests/ember-data__request/tests/integration/abort-test.ts similarity index 82% rename from tests/request/tests/integration/abort-test.ts rename to tests/ember-data__request/tests/integration/abort-test.ts index c43f6e6c143..0e656e70f09 100644 --- a/tests/request/tests/integration/abort-test.ts +++ b/tests/ember-data__request/tests/integration/abort-test.ts @@ -1,8 +1,7 @@ -import { module, test } from 'qunit'; - +import type { Future, Handler, NextFn, RequestInfo } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Future, Handler, NextFn, RequestInfo } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Abort', function () { test('We can abort requests', async function (assert) { @@ -10,7 +9,7 @@ module('RequestManager | Abort', function () { const manager = new RequestManager(); const handler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal'); const result = await fetch(context.request.url!, context.request); @@ -35,7 +34,7 @@ module('RequestManager | Abort', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler1'); @@ -44,7 +43,7 @@ module('RequestManager | Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); const result = await fetch(context.request.url!, context.request); @@ -69,7 +68,7 @@ module('RequestManager | Abort', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); const future2 = next(context.request); const future3 = next(context.request); @@ -81,7 +80,7 @@ module('RequestManager | Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); const result = await fetch(context.request.url!, context.request); @@ -111,7 +110,7 @@ module('RequestManager | Abort', function () { let controller!: AbortController; const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const request = Object.assign({}, context.request) as RequestInfo; delete request.signal; controller = new AbortController(); @@ -123,13 +122,9 @@ module('RequestManager | Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive an abort signal in handler2'); - assert.strictEqual( - context.request.signal, - controller.signal, - 'we receive the expected abort signal in handler2' - ); + assert.equal(context.request.signal, controller.signal, 'we receive the expected abort signal in handler2'); resolvePre(); await beforeFetch; @@ -152,7 +147,7 @@ module('RequestManager | Abort', function () { assert.ok(false, 'aborting a request should result in the promise rejecting'); } catch (e) { assert.true(e instanceof Error); - assert.strictEqual((e as Error).message, 'Root Controller Aborted', 'We got the correct error'); + assert.equal((e as Error).message, 'Root Controller Aborted', 'We got the correct error'); } }); @@ -167,7 +162,7 @@ module('RequestManager | Abort', function () { let controller!: AbortController; const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const request = Object.assign({}, context.request) as RequestInfo; signal = request.signal!; delete request.signal; @@ -180,13 +175,9 @@ module('RequestManager | Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive an abort signal in handler2'); - assert.strictEqual( - context.request.signal, - controller.signal, - 'we receive the expected abort signal in handler2' - ); + assert.equal(context.request.signal, controller.signal, 'we receive the expected abort signal in handler2'); resolvePre(); await beforeFetch; @@ -209,7 +200,7 @@ module('RequestManager | Abort', function () { assert.ok(false, 'aborting a request should result in the promise rejecting'); } catch (e) { assert.true(e instanceof Error); - assert.strictEqual((e as Error).message, 'Root Controller Aborted', 'We got the correct error'); + assert.equal((e as Error).message, 'Root Controller Aborted', 'We got the correct error'); assert.false(signal.aborted, 'The root signal is not aborted'); } }); @@ -219,7 +210,7 @@ module('RequestManager | Abort', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler1'); @@ -228,7 +219,7 @@ module('RequestManager | Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); const request: RequestInfo = Object.assign({}, context.request) as RequestInfo; delete request.signal; diff --git a/tests/request/tests/integration/custom-abort-test.ts b/tests/ember-data__request/tests/integration/custom-abort-test.ts similarity index 76% rename from tests/request/tests/integration/custom-abort-test.ts rename to tests/ember-data__request/tests/integration/custom-abort-test.ts index 53e7fc0b890..d63a60f2468 100644 --- a/tests/request/tests/integration/custom-abort-test.ts +++ b/tests/ember-data__request/tests/integration/custom-abort-test.ts @@ -1,8 +1,7 @@ -import { module, test } from 'qunit'; - +import type { Future, Handler, NextFn, RequestInfo } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Future, Handler, NextFn, RequestInfo } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Custom Abort', function () { test('We can abort requests', async function (assert) { @@ -11,11 +10,11 @@ module('RequestManager | Custom Abort', function () { const controller = new AbortController(); const handler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal'); - assert.strictEqual(context.request.signal, controller.signal, 'we receive the correct signal'); + assert.equal(context.request.signal, controller.signal, 'we receive the correct signal'); // @ts-expect-error - assert.strictEqual(context.request.controller, undefined, 'we do not receive the controller'); + assert.equal(context.request.controller, undefined, 'we do not receive the controller'); const result = await fetch(context.request.url!, context.request); return result.json() as T; @@ -40,22 +39,22 @@ module('RequestManager | Custom Abort', function () { const controller = new AbortController(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler1'); - assert.strictEqual(context.request.signal, controller.signal, 'we receive the correct signal'); + assert.equal(context.request.signal, controller.signal, 'we receive the correct signal'); // @ts-expect-error - assert.strictEqual(context.request.controller, undefined, 'we do not receive the controller'); + assert.equal(context.request.controller, undefined, 'we do not receive the controller'); return (await future).content; }, }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); - assert.strictEqual(context.request.signal, controller.signal, 'we receive the correct signal'); + assert.equal(context.request.signal, controller.signal, 'we receive the correct signal'); // @ts-expect-error - assert.strictEqual(context.request.controller, undefined, 'we do not receive the controller'); + assert.equal(context.request.controller, undefined, 'we do not receive the controller'); const result = await fetch(context.request.url!, context.request); return result.json() as T; @@ -79,7 +78,7 @@ module('RequestManager | Custom Abort', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler1'); const controller = new AbortController(); const future = next(Object.assign({ controller }, context.request, { signal: controller.signal })); @@ -89,7 +88,7 @@ module('RequestManager | Custom Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); const result = await fetch(context.request.url!, context.request); @@ -114,7 +113,7 @@ module('RequestManager | Custom Abort', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const controller = new AbortController(); const future = next(Object.assign({ controller }, context.request)); assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler1'); @@ -124,7 +123,7 @@ module('RequestManager | Custom Abort', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.true(context.request.signal instanceof AbortSignal, 'we receive the abort signal in handler2'); const request: RequestInfo = Object.assign({}, context.request) as RequestInfo; delete request.signal; diff --git a/tests/request/tests/integration/error-propagation-test.ts b/tests/ember-data__request/tests/integration/error-propagation-test.ts similarity index 81% rename from tests/request/tests/integration/error-propagation-test.ts rename to tests/ember-data__request/tests/integration/error-propagation-test.ts index d7353c1c660..4f0d65563b6 100644 --- a/tests/request/tests/integration/error-propagation-test.ts +++ b/tests/ember-data__request/tests/integration/error-propagation-test.ts @@ -1,8 +1,7 @@ -import { module, test } from 'qunit'; - +import type { Future, Handler, NextFn, StructuredErrorDocument } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Future, Handler, NextFn, StructuredErrorDocument } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; function isErrorDoc(e: Error | unknown | StructuredErrorDocument): e is StructuredErrorDocument { return Boolean(e && e instanceof Error && 'request' in e); @@ -13,39 +12,39 @@ module('RequestManager | Error Propagation', function () { const manager = new RequestManager(); const catchingHandler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.ok(true, 'catching handler triggered'); try { // await to catch, else error is curried return await next(context.request); } catch (e) { - assert.strictEqual((e as Error).message, 'Oops!', 'We caught the error'); + assert.equal((e as Error).message, 'Oops!', 'We caught the error'); return 'We are happy' as T; } }, }; const throwingHandler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { assert.ok(true, 'throwing handler triggered'); throw new Error('Oops!'); }, }; manager.use([catchingHandler, throwingHandler]); const { content } = await manager.request({ url: '/wat' }); - assert.strictEqual(content, 'We are happy', 'we caught and handled the error'); + assert.equal(content, 'We are happy', 'we caught and handled the error'); }); test('Errors thrown by a handler curry the request properly', async function (assert) { assert.expect(4); const manager = new RequestManager(); const curryingHandler: Handler = { - request(context: Context, next: NextFn): Promise | Future { + request(context: RequestContext, next: NextFn): Promise | Future { assert.ok(true, 'catching handler triggered'); return next({ url: '/curried' }); }, }; const throwingHandler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { assert.ok(true, 'throwing handler triggered'); throw new Error('Oops!'); }, @@ -68,13 +67,13 @@ module('RequestManager | Error Propagation', function () { const manager = new RequestManager(); const catchingHandler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.ok(true, 'catching handler triggered'); return await next({ url: '/curried' }); }, }; const throwingHandler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { assert.ok(true, 'throwing handler triggered'); throw new Error('Oops!'); }, @@ -97,13 +96,13 @@ module('RequestManager | Error Propagation', function () { const manager = new RequestManager(); const catchingHandler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { assert.ok(true, 'catching handler triggered'); return await next({ url: '/curried' }); }, }; const throwingHandler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { assert.ok(true, 'throwing handler triggered'); throw new Error('Oops!'); }, diff --git a/tests/ember-data__request/tests/integration/fetch-handler-test.ts b/tests/ember-data__request/tests/integration/fetch-handler-test.ts new file mode 100644 index 00000000000..cea431bdc6f --- /dev/null +++ b/tests/ember-data__request/tests/integration/fetch-handler-test.ts @@ -0,0 +1,194 @@ +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import { buildBaseURL } from '@ember-data/request-utils'; +import { module, test } from '@warp-drive/diagnostic'; +import { mock, MockServerHandler } from '@warp-drive/holodeck'; +import { GET } from '@warp-drive/holodeck/mock'; + +const RECORD = false; + +function isNetworkError(e: unknown): asserts e is Error & { + status: number; + statusText: string; + code: number; + name: string; + isRequestError: boolean; + content?: object; + errors?: object[]; +} { + if (!(e instanceof Error)) { + throw new Error('Expected a network error'); + } +} + +module('RequestManager | Fetch Handler', function (hooks) { + test('Parses 200 Responses', async function (assert) { + const manager = new RequestManager(); + manager.use([new MockServerHandler(this), Fetch]); + + await GET( + this, + 'users/1', + () => ({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Chris Thoburn', + }, + }, + }), + { RECORD } + ); + + const doc = await manager.request({ url: buildBaseURL({ resourcePath: 'users/1' }) }); + const serialized = JSON.parse(JSON.stringify(doc)) as unknown; + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + serialized.response.headers = (serialized.response.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); + }); + + assert.deepEqual( + serialized, + { + content: { + data: { + attributes: { + name: 'Chris Thoburn', + }, + id: '1', + type: 'user', + }, + }, + request: { + url: buildBaseURL({ resourcePath: 'users/1' }), + }, + response: { + headers: [ + ['cache-control', 'no-store'], + ['content-type', 'application/vnd.api+json'], + ], + ok: true, + redirected: false, + status: 200, + statusText: '', + type: 'default', + url: '', + }, + }, + 'The response is processed correctly' + ); + }); + + test('It provides useful errors', async function (assert) { + const manager = new RequestManager(); + manager.use([new MockServerHandler(this), Fetch]); + + await mock( + this, + () => ({ + url: 'users/1', + status: 404, + headers: {}, + method: 'GET', + statusText: 'Not Found', + body: null, + response: { + errors: [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + }, + }), + RECORD + ); + + try { + await manager.request({ url: buildBaseURL({ resourcePath: 'users/1' }) }); + assert.ok(false, 'Should have thrown'); + } catch (e) { + isNetworkError(e); + assert.true(e instanceof AggregateError, 'The error is an AggregateError'); + assert.equal( + e.message, + `[404 Not Found] GET (cors) - ${buildBaseURL({ resourcePath: 'users/1' })}`, + 'The error message is correct' + ); + assert.equal(e.status, 404, 'The error status is correct'); + assert.equal(e.statusText, 'Not Found', 'The error statusText is correct'); + assert.equal(e.code, 404, 'The error code is correct'); + assert.equal(e.name, 'NotFoundError', 'The error code is correct'); + assert.true(e.isRequestError, 'The error is a request error'); + + // error.content is present + assert.deepEqual( + e.content, + { + errors: [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + }, + 'The error.content is present' + ); + + // error.errors is present + assert.deepEqual( + e.errors, + [ + { + status: '404', + title: 'Not Found', + detail: 'The resource does not exist.', + }, + ], + 'The error.errors is present' + ); + } + }); + + test('It provides useful error during abort', async function (assert) { + const manager = new RequestManager(); + manager.use([new MockServerHandler(this), Fetch]); + + await GET( + this, + 'users/1', + () => ({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Chris Thoburn', + }, + }, + }), + { RECORD } + ); + + try { + const future = manager.request({ url: buildBaseURL({ resourcePath: 'users/1' }) }); + await Promise.resolve(); + future.abort(); + await future; + assert.ok(false, 'Should have thrown'); + } catch (e) { + isNetworkError(e); + assert.true(e instanceof DOMException, 'The error is a DOMException'); + assert.equal(e.message, 'The user aborted a request.', 'The error message is correct'); + assert.equal(e.status, 20, 'The error status is correct'); + assert.equal(e.statusText, 'Aborted', 'The error statusText is correct'); + assert.equal(e.code, 20, 'The error code is correct'); + assert.equal(e.name, 'AbortError', 'The error name is correct'); + assert.true(e.isRequestError, 'The error is a request error'); + } + }); +}); diff --git a/tests/request/tests/integration/graceful-dev-errors-test.ts b/tests/ember-data__request/tests/integration/graceful-dev-errors-test.ts similarity index 91% rename from tests/request/tests/integration/graceful-dev-errors-test.ts rename to tests/ember-data__request/tests/integration/graceful-dev-errors-test.ts index 343e5af8bec..bd29d92a8a3 100644 --- a/tests/request/tests/integration/graceful-dev-errors-test.ts +++ b/tests/ember-data__request/tests/integration/graceful-dev-errors-test.ts @@ -1,12 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ - -import { module, test } from 'qunit'; - +import type { Handler, NextFn } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Handler, NextFn } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Graceful Errors', function () { test('We error meaningfully for `.use()`', function (assert) { @@ -17,7 +12,7 @@ module('RequestManager | Graceful Errors', function () { }, }; try { - // @ts-ignore-error + // @ts-expect-error manager.use(handler); assert.ok(false, 'we should error when not passing an array'); } catch (e: unknown) { @@ -40,12 +35,12 @@ module('RequestManager | Graceful Errors', function () { await manager.request({ url: '/wat' }); try { - // @ts-ignore-error + // @ts-expect-error manager.use(handler); assert.ok(false, 'we should error when not passing an array'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Cannot add a Handler to a RequestManager after a request has been made`, (e as Error).message, `${(e as Error).message} does not match the expected error` @@ -61,7 +56,7 @@ module('RequestManager | Graceful Errors', function () { }, }; try { - // @ts-ignore-error + // @ts-expect-error manager.use(handler); assert.ok(false, 'we should error when not passing an array'); } catch (e: unknown) { @@ -89,7 +84,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the handler returns undefined'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Expected handler.request to return a promise, instead received a synchronous value.`, (e as Error).message, `${(e as Error).message} does not match the expected error` @@ -112,7 +107,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the handler returns undefined'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Expected handler.request to return a promise, instead received undefined.`, (e as Error).message, `${(e as Error).message} does not match the expected error` @@ -123,7 +118,7 @@ module('RequestManager | Graceful Errors', function () { test('We error meaningfully for empty requests', async function (assert) { const manager = new RequestManager(); const handler: Handler = { - request(_context: Context, _next: NextFn): Promise { + request(_context: RequestContext, _next: NextFn): Promise { return Promise.resolve('done' as T); }, }; @@ -135,7 +130,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the request is missing'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error when the request is missing'); - assert.strictEqual( + assert.equal( (e as Error).message, 'Expected RequestManager.request() to be called with a request, but none was provided.', `Expected: ${(e as Error).message} - to match the expected error` @@ -148,7 +143,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the request is not an object'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error when the request is not an object'); - assert.strictEqual( + assert.equal( (e as Error).message, 'The `request` passed to `RequestManager.request()` should be an object, received `array`', `Expected: ${(e as Error).message} - to match the expected error` @@ -160,7 +155,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the request has no keys'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error when the request has no keys'); - assert.strictEqual( + assert.equal( (e as Error).message, 'The `request` passed to `RequestManager.request()` was empty (`{}`). Requests need at least one valid key.', `Expected: ${(e as Error).message} - to match the expected error` @@ -176,7 +171,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when no handler is present'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( (e as Error).message, `No handler was able to handle this request.`, `Expected ${(e as Error).message} to match the expected error` @@ -187,7 +182,7 @@ module('RequestManager | Graceful Errors', function () { test('We error meaningfully for invalid next', async function (assert) { const manager = new RequestManager(); const handler = { - request(req: Context, next: NextFn) { + request(req: RequestContext, next: NextFn) { return next(req.request); }, }; @@ -197,7 +192,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when no handler is present'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( (e as Error).message, `No handler was able to handle this request.`, `Expected ${(e as Error).message} to match the expected error` @@ -208,7 +203,7 @@ module('RequestManager | Graceful Errors', function () { test('We error meaningfully for misshapen requests', async function (assert) { const manager = new RequestManager(); const handler: Handler = { - request(_context: Context, _next: NextFn): Promise { + request(_context: RequestContext, _next: NextFn): Promise { return Promise.resolve('done' as T); }, }; @@ -234,6 +229,7 @@ module('RequestManager | Graceful Errors', function () { integrity: false, // @ts-expect-error keepalive: 'yes', + // @ts-expect-error method: 'get', // @ts-expect-error mode: 'find-out', @@ -247,7 +243,7 @@ module('RequestManager | Graceful Errors', function () { assert.ok(false, 'we should error when the handler returns undefined'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Invalid Request passed to \`RequestManager.request()\`. The following issues were found: @@ -280,7 +276,7 @@ The following issues were found: assert.ok(false, 'we should error when the handler returns undefined'); } catch (e: unknown) { assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Invalid Request passed to \`RequestManager.request()\`. The following issues were found: diff --git a/tests/request/tests/integration/graceful-dev-handler-errors-test.ts b/tests/ember-data__request/tests/integration/graceful-dev-handler-errors-test.ts similarity index 89% rename from tests/request/tests/integration/graceful-dev-handler-errors-test.ts rename to tests/ember-data__request/tests/integration/graceful-dev-handler-errors-test.ts index 2e8a63d1bdc..2d8ef02de74 100644 --- a/tests/request/tests/integration/graceful-dev-handler-errors-test.ts +++ b/tests/ember-data__request/tests/integration/graceful-dev-handler-errors-test.ts @@ -1,12 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ - -import { module, test } from 'qunit'; - +import type { Handler, NextFn } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Handler, NextFn } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Graceful Handler Errors', function () { test('We error meaningfully for empty requests', async function (assert) { @@ -14,7 +9,7 @@ module('RequestManager | Graceful Handler Errors', function () { let called = false; let nextArg: unknown = undefined; const handler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { called = true; // @ts-expect-error return nextArg ? next(nextArg) : next(); @@ -30,7 +25,7 @@ module('RequestManager | Graceful Handler Errors', function () { } catch (e: unknown) { assert.true(called, 'we invoked the handler'); assert.true(e instanceof Error, 'We throw an error when the request is missing'); - assert.strictEqual( + assert.equal( (e as Error).message, 'Expected next() to be called with a request, but none was provided.', `Expected: ${(e as Error).message} - to match the expected error` @@ -45,7 +40,7 @@ module('RequestManager | Graceful Handler Errors', function () { } catch (e: unknown) { assert.true(called, 'we invoked the handler'); assert.true(e instanceof Error, 'We throw an error when the request is not an object'); - assert.strictEqual( + assert.equal( (e as Error).message, 'The `request` passed to `next()` should be an object, received `array`', `Expected: ${(e as Error).message} - to match the expected error` @@ -60,7 +55,7 @@ module('RequestManager | Graceful Handler Errors', function () { } catch (e: unknown) { assert.true(called, 'we invoked the handler'); assert.true(e instanceof Error, 'We throw an error when the request has no keys'); - assert.strictEqual( + assert.equal( (e as Error).message, 'The `request` passed to `next()` was empty (`{}`). Requests need at least one valid key.', `Expected: ${(e as Error).message} - to match the expected error` @@ -73,7 +68,7 @@ module('RequestManager | Graceful Handler Errors', function () { let called = false; let nextArg: unknown = undefined; const handler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { called = true; // @ts-expect-error return nextArg ? next(nextArg) : next(); @@ -103,7 +98,7 @@ module('RequestManager | Graceful Handler Errors', function () { } catch (e: unknown) { assert.true(called, 'we invoked the handler'); assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Invalid Request passed to \`next()\`. The following issues were found: @@ -132,7 +127,7 @@ The following issues were found: let called = false; let nextArg: unknown = undefined; const handler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { called = true; // @ts-expect-error return nextArg ? next(nextArg) : next(); @@ -150,7 +145,7 @@ The following issues were found: } catch (e: unknown) { assert.true(called, 'we invoked the handler'); assert.true(e instanceof Error, 'We throw an error'); - assert.strictEqual( + assert.equal( `Invalid Request passed to \`next()\`. The following issues were found: diff --git a/tests/request/tests/integration/immutability-test.ts b/tests/ember-data__request/tests/integration/immutability-test.ts similarity index 76% rename from tests/request/tests/integration/immutability-test.ts rename to tests/ember-data__request/tests/integration/immutability-test.ts index 4fb0ce4d5e5..2011b00feb9 100644 --- a/tests/request/tests/integration/immutability-test.ts +++ b/tests/ember-data__request/tests/integration/immutability-test.ts @@ -1,14 +1,13 @@ -import { module, test } from 'qunit'; - +import type { Handler, NextFn } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Handler, NextFn } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Immutability', function () { test('RequestInfo passed to a handler is Immutable', async function (assert) { const manager = new RequestManager(); const handler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { // @ts-expect-error context.request.integrity = 'some val'; return Promise.resolve('hello' as T); @@ -30,7 +29,7 @@ module('RequestManager | Immutability', function () { test('Headers in RequestInfo passed to a handler are Immutable', async function (assert) { const manager = new RequestManager(); const handler: Handler = { - request(context: Context, next: NextFn) { + request(context: RequestContext, next: NextFn) { context.request.headers!.append('house', 'home'); return Promise.resolve('hello' as T); }, @@ -41,7 +40,7 @@ module('RequestManager | Immutability', function () { await manager.request({ url: '/foo', headers: new Headers([['foo', 'bar']]) }); assert.ok(false, 'we should have erred'); } catch (e) { - assert.strictEqual( + assert.equal( (e as Error).message, 'Cannot Mutate Immutatable Headers, use headers.clone to get a copy', `expected ${(e as Error).message} to match the expected error` @@ -51,9 +50,11 @@ module('RequestManager | Immutability', function () { test('Headers in RequestInfo passed to a handler may be edited after cloning', async function (assert) { const manager = new RequestManager(); const handler: Handler = { - request(context: Context, next: NextFn) { - const headers = context.request.headers!.clone(); + request(context: RequestContext, next: NextFn) { + const headers = new Headers(context.request.headers); headers.append('house', 'home'); + // @ts-expect-error Types are wrong: Property 'entries' does not exist on type 'Headers'. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call return Promise.resolve([...headers.entries()] as T); }, }; diff --git a/tests/ember-data__request/tests/integration/request-manager-test.ts b/tests/ember-data__request/tests/integration/request-manager-test.ts new file mode 100644 index 00000000000..cc4f435d56d --- /dev/null +++ b/tests/ember-data__request/tests/integration/request-manager-test.ts @@ -0,0 +1,7 @@ +import { module, test } from '@warp-drive/diagnostic'; + +module('RequestManager', function () { + test('Test Suit Configured', function (assert) { + assert.ok('We are configured'); + }); +}); diff --git a/tests/ember-data__request/tests/integration/response-currying-test.ts b/tests/ember-data__request/tests/integration/response-currying-test.ts new file mode 100644 index 00000000000..ee440640d89 --- /dev/null +++ b/tests/ember-data__request/tests/integration/response-currying-test.ts @@ -0,0 +1,260 @@ +import type { Handler, NextFn } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; + +const IGNORED_HEADERS = new Set(['connection', 'keep-alive', 'content-length', 'date', 'etag', 'last-modified']); + +module('RequestManager | Response Currying', function () { + test('We curry response when setResponse is not called', async function (assert) { + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await next(context.request); + return response.content; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; + // @ts-expect-error + serialized.headers = (serialized.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !IGNORED_HEADERS.has(v[0]); + }); + // @ts-expect-error port is unstable in CI + delete serialized.url; + + assert.deepEqual( + serialized, + { + ok: true, + redirected: false, + headers: [ + ['content-type', 'application/json;charset=utf-8'], + // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], + // ['etag', 'W/"39-1849db13af9"'], + // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], + ], + status: 200, + statusText: 'OK', + type: 'basic', + }, + 'The response is processed correctly' + ); + }); + + test('We do not curry response when we call next multiple times', async function (assert) { + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn): Promise { + await next(context.request); + await next(context.request); + return (await next(context.request)).content; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + + assert.equal(doc.response, null, 'The response is processed correctly'); + }); + + test('We curry when we return directly', async function (assert) { + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn): Promise { + return next(context.request) as unknown as Promise; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; + // @ts-expect-error + serialized.headers = (serialized.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !IGNORED_HEADERS.has(v[0]); + }); + // @ts-expect-error port is unstable in CI + delete serialized.url; + + assert.deepEqual( + serialized, + { + ok: true, + redirected: false, + headers: [ + ['content-type', 'application/json;charset=utf-8'], + // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], + // ['etag', 'W/"39-1849db13af9"'], + // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], + ], + status: 200, + statusText: 'OK', + type: 'basic', + }, + 'The response is processed correctly' + ); + }); + + test('We can intercept Response', async function (assert) { + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn): Promise { + const doc = await next(context.request); + + const response = Object.assign({}, doc.response, { ok: false }); + context.setResponse(response); + + return doc.content; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; + // @ts-expect-error + serialized.headers = (serialized.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !IGNORED_HEADERS.has(v[0]); + }); + // @ts-expect-error port is unstable in CI + delete serialized.url; + + assert.deepEqual( + serialized, + { + ok: false, + redirected: false, + headers: [ + ['content-type', 'application/json;charset=utf-8'], + // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], + // ['etag', 'W/"39-1849db13af9"'], + // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], + ], + status: 200, + statusText: 'OK', + type: 'basic', + }, + 'The response is processed correctly' + ); + }); + + test("We can can't mutate Response", async function (assert) { + assert.expect(3); + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn): Promise { + const doc = await next(context.request); + + try { + // @ts-expect-error + doc.response!.ok = false; + assert.ok(false, 'we should be immutable'); + } catch { + assert.ok(true, 'we are immutable'); + } + + try { + doc.response!.headers.append('foo', 'bar'); + assert.ok(false, 'we should be immutable'); + } catch { + assert.ok(true, 'we are immutable'); + } + + return doc.content; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; + // @ts-expect-error + serialized.headers = (serialized.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !IGNORED_HEADERS.has(v[0]); + }); + // @ts-expect-error port is unstable in CI + delete serialized.url; + + assert.deepEqual( + serialized, + { + ok: true, + redirected: false, + headers: [ + ['content-type', 'application/json;charset=utf-8'], + // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], + // ['etag', 'W/"39-1849db13af9"'], + // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], + ], + status: 200, + statusText: 'OK', + type: 'basic', + }, + 'The response is processed correctly' + ); + }); + + test('We can set response to null', async function (assert) { + const manager = new RequestManager(); + const handler1: Handler = { + async request(context: RequestContext, next: NextFn): Promise { + const doc = await next(context.request); + + context.setResponse(null); + + return doc.content; + }, + }; + const handler2: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler1, handler2]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + + assert.equal(doc.response, null, 'The response is processed correctly'); + }); +}); diff --git a/tests/ember-data__request/tests/integration/response-test.ts b/tests/ember-data__request/tests/integration/response-test.ts new file mode 100644 index 00000000000..df3e033eaf4 --- /dev/null +++ b/tests/ember-data__request/tests/integration/response-test.ts @@ -0,0 +1,48 @@ +import type { Handler, NextFn } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; + +const IGNORED_HEADERS = new Set(['connection', 'keep-alive', 'content-length', 'date', 'etag', 'last-modified']); + +module('RequestManager | Response', function () { + test('Handlers may set response via Response', async function (assert) { + const manager = new RequestManager(); + const handler: Handler = { + async request(context: RequestContext, next: NextFn) { + const response = await fetch(context.request.url!, context.request); + context.setResponse(response); + return response.json() as Promise; + }, + }; + manager.use([handler]); + + const doc = await manager.request({ url: '../assets/demo-fetch.json' }); + const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; + // @ts-expect-error + serialized.headers = (serialized.headers as [string, string][]).filter((v) => { + // don't test headers that change every time + return !IGNORED_HEADERS.has(v[0]); + }); + // @ts-expect-error port is unstable in CI + delete serialized.url; + + assert.deepEqual( + serialized, + { + ok: true, + redirected: false, + headers: [ + ['content-type', 'application/json;charset=utf-8'], + // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], + // ['etag', 'W/"39-1849db13af9"'], + // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], + ], + status: 200, + statusText: 'OK', + type: 'basic', + }, + 'The response is processed correctly' + ); + }); +}); diff --git a/tests/ember-data__request/tests/integration/service-test.ts b/tests/ember-data__request/tests/integration/service-test.ts new file mode 100644 index 00000000000..2a50290d42e --- /dev/null +++ b/tests/ember-data__request/tests/integration/service-test.ts @@ -0,0 +1,33 @@ +import { getOwner } from '@ember/application'; +import Service, { inject as service } from '@ember/service'; +import type { TestContext } from '@ember/test-helpers'; + +import Resolver from 'ember-resolver'; + +import RequestManager from '@ember-data/request'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('RequestManager | Ember Service Setup', function (hooks) { + setupTest(hooks, { resolver: new Resolver() }); + + test('We can register RequestManager as a service', function (this: TestContext, assert) { + this.owner.register('service:request', RequestManager); + const manager = this.owner.lookup('service:request'); + assert.ok(manager instanceof RequestManager, 'We instantiated'); + }); + + test('We can use injections when registering the RequestManager as a service', function (this: TestContext, assert) { + class CustomManager extends RequestManager { + @service cache; + } + this.owner.register('service:request', CustomManager); + class Cache extends Service {} + this.owner.register('service:cache', Cache); + const manager = this.owner.lookup('service:request') as unknown as CustomManager; + assert.ok(manager instanceof RequestManager, 'We instantiated'); + assert.ok(manager instanceof CustomManager, 'We instantiated'); + assert.ok(manager.cache instanceof Cache, 'We can utilize injections'); + assert.equal(getOwner(manager), this.owner, 'The manager correctly sets owner'); + }); +}); diff --git a/tests/ember-data__request/tests/integration/setup-test.ts b/tests/ember-data__request/tests/integration/setup-test.ts new file mode 100644 index 00000000000..aa3816f1909 --- /dev/null +++ b/tests/ember-data__request/tests/integration/setup-test.ts @@ -0,0 +1,90 @@ +import type { NextFn } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; + +module('RequestManager | Basic Setup', function () { + test('We can call new RequestManager() with no args', function (assert) { + const manager = new RequestManager(); + assert.ok(manager instanceof RequestManager, 'We instantiated'); + }); + + test('We can call RequestManager.create() with no args', function (assert) { + const manager = RequestManager.create(); + assert.ok(manager instanceof RequestManager, 'We instantiated'); + }); + + test('We can register a handler with `.use()`', async function (assert) { + const manager = new RequestManager(); + let calls = 0; + manager.use([ + { + request(req: RequestContext, next: NextFn) { + calls++; + return Promise.resolve('success!' as T); + }, + }, + ]); + const req = { + url: '/foos', + }; + const result = await manager.request(req); + assert.equal(calls, 1, 'we called our handler'); + assert.equal(JSON.stringify(result.request), JSON.stringify(req)); + assert.equal(result.content, 'success!', 'we returned the expected result'); + }); + + test('We can register multiple handlers with `.use()`', async function (assert) { + const manager = new RequestManager(); + let calls = 0; + let callsB = 0; + manager.use([ + { + async request(req: RequestContext, next: NextFn) { + calls++; + const outcome = await next(req.request); + return outcome.content; + }, + }, + { + request(req: RequestContext, next: NextFn) { + callsB++; + return Promise.resolve('success!' as T); + }, + }, + ]); + const req = { + url: '/foos', + }; + const result = await manager.request(req); + assert.equal(calls, 1, 'we called our handler'); + assert.equal(callsB, 1, 'we called our next handler'); + assert.equal(JSON.stringify(result.request), JSON.stringify(req)); + assert.equal(result.content, 'success!', 'we returned the expected result'); + }); + + test('We can register the same handler more than once with `.use()`', async function (assert) { + const manager = new RequestManager(); + let calls = 0; + + const handler = { + async request(req: RequestContext, next: NextFn) { + calls++; + if (calls === 2) { + return Promise.resolve('success!' as T); + } + const outcome = await next(req.request); + return outcome.content; + }, + }; + + manager.use([handler, handler]); + const req = { + url: '/foos', + }; + const result = await manager.request(req); + assert.equal(calls, 2, 'we called our handler'); + assert.equal(JSON.stringify(result.request), JSON.stringify(req)); + assert.equal(result.content, 'success!', 'we returned the expected result'); + }); +}); diff --git a/tests/ember-data__request/tests/integration/stateful-handler-test.ts b/tests/ember-data__request/tests/integration/stateful-handler-test.ts new file mode 100644 index 00000000000..a76d27f198d --- /dev/null +++ b/tests/ember-data__request/tests/integration/stateful-handler-test.ts @@ -0,0 +1,57 @@ +import { setOwner } from '@ember/owner'; +import { service } from '@ember/service'; +import type { TestContext } from '@ember/test-helpers'; + +import Resolver from 'ember-resolver'; + +import type { NextFn } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; +import { setupTest } from '@warp-drive/diagnostic/ember'; + +module('RequestManager | Stateful Handlers', function (hooks) { + setupTest(hooks, { resolver: new Resolver() }); + + test('We can register a handler with `.use()`', async function (this: TestContext, assert) { + const manager = new RequestManager(); + let calls = 0; + + this.owner.register( + 'service:intl', + class { + t(key: string) { + return key + ' was intl-ed'; + } + + static create() { + return new this(); + } + } + ); + + class MyHandler { + @service declare intl: { t: (key: string) => string }; + + request(req: RequestContext, next: NextFn) { + calls++; + return Promise.resolve(this.intl.t('success!') as T); + } + } + + const handler = new MyHandler(); + // const owner = getOwner(this); // where "this" is the store + setOwner(handler, this.owner); + // if you need to handle destroy logic, you can register a destructor + // registerDestructor(handler, () => {}); + + manager.use([handler]); + const req = { + url: '/foos', + }; + const result = await manager.request(req); + assert.equal(calls, 1, 'we called our handler'); + assert.equal(JSON.stringify(result.request), JSON.stringify(req)); + assert.equal(result.content, 'success! was intl-ed', 'we returned the expected result'); + }); +}); diff --git a/tests/request/tests/integration/streams-test.ts b/tests/ember-data__request/tests/integration/streams-test.ts similarity index 83% rename from tests/request/tests/integration/streams-test.ts rename to tests/ember-data__request/tests/integration/streams-test.ts index 9fda6f67a82..f3f13b47fa3 100644 --- a/tests/request/tests/integration/streams-test.ts +++ b/tests/ember-data__request/tests/integration/streams-test.ts @@ -1,8 +1,7 @@ -import { module, test } from 'qunit'; - +import type { Future, Handler, NextFn } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Future, Handler, NextFn } from '@ember-data/request/-private/types'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import { module, test } from '@warp-drive/diagnostic'; module('RequestManager | Streams', function () { test('We can read the stream returned from a handler', async function (assert) { @@ -10,7 +9,7 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -44,7 +43,7 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); context.setStream(future.getStream()); @@ -54,7 +53,7 @@ module('RequestManager | Streams', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -88,7 +87,7 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); const stream = await future.getStream(); assert.true(stream instanceof ReadableStream, 'we receive the stream'); @@ -99,7 +98,7 @@ module('RequestManager | Streams', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -114,7 +113,7 @@ module('RequestManager | Streams', function () { const future = manager.request({ url: '../assets/demo-fetch.json' }); const stream = await future.getStream(); - assert.strictEqual(stream, null, 'we receive the null as the stream'); + assert.equal(stream, null, 'we receive the null as the stream'); const result = await future; assert.deepEqual( result.content, @@ -133,14 +132,14 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const future = next(context.request); return (await future).content; }, }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -173,13 +172,13 @@ module('RequestManager | Streams', function () { assert.expect(2); const manager = new RequestManager(); const handler1: Handler = { - request(context: Context, next: NextFn): Promise | Future { + request(context: RequestContext, next: NextFn): Promise | Future { return next(context.request); }, }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -213,7 +212,7 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const a = await next(context.request); const b = await next(context.request); return a.content || b.content; @@ -221,7 +220,7 @@ module('RequestManager | Streams', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { @@ -236,7 +235,7 @@ module('RequestManager | Streams', function () { const future = manager.request({ url: '../assets/demo-fetch.json' }); const stream = await future.getStream(); - assert.strictEqual(stream, null, 'we do not receive the stream'); + assert.equal(stream, null, 'we do not receive the stream'); const result = await future; assert.deepEqual( result.content, @@ -255,7 +254,7 @@ module('RequestManager | Streams', function () { const manager = new RequestManager(); const handler1: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { await next(context.request); await next(context.request); return next(context.request); @@ -263,7 +262,7 @@ module('RequestManager | Streams', function () { }; const handler2: Handler = { // @ts-expect-error - async request(context: Context, next: NextFn): Promise | Future { + async request(context: RequestContext, next: NextFn): Promise | Future { const result = await fetch(context.request.url!, context.request); if (result.body) { diff --git a/tests/ember-data__request/tests/test-helper.js b/tests/ember-data__request/tests/test-helper.js new file mode 100644 index 00000000000..90ae7c5a125 --- /dev/null +++ b/tests/ember-data__request/tests/test-helper.js @@ -0,0 +1,33 @@ +import { setBuildURLConfig } from '@ember-data/request-utils'; +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { configure } from '@warp-drive/diagnostic/ember'; +import { start } from '@warp-drive/diagnostic/runners/dom'; +import { setConfig, setTestId } from '@warp-drive/holodeck'; + +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; +setBuildURLConfig({ + host: MockHost, + namespace: '', +}); +setConfig({ host: MockHost }); + +setupGlobalHooks((hooks) => { + hooks.beforeEach(function (assert) { + setTestId(this, assert.test.testId); + }); + hooks.afterEach(function () { + setTestId(this, null); + }); +}); + +configure(); + +start({ + tryCatch: false, + debug: false, + concurrency: 10, + groupLogs: false, + instrument: true, + hideReport: true, + useDiagnostic: true, +}); diff --git a/tests/ember-data__request/tsconfig.json b/tests/ember-data__request/tsconfig.json new file mode 100644 index 00000000000..ba62fea52b7 --- /dev/null +++ b/tests/ember-data__request/tsconfig.json @@ -0,0 +1,61 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "experimentalDecorators": true, + // TODO: Reenable this + "noImplicitAny": false, + "noImplicitOverride": false, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], + "@warp-drive/diagnostic": ["../../packages/diagnostic/unstable-preview-types"], + "@warp-drive/diagnostic/*": ["../../packages/diagnostic/unstable-preview-types/*"], + "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types"], + "@warp-drive/holodeck/*": ["../../packages/holodeck/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/diagnostic" + }, + { + "path": "../../packages/holodeck" + } + ] +} diff --git a/tests/ember-data__serializer/README.md b/tests/ember-data__serializer/README.md new file mode 100644 index 00000000000..5c89cb9a497 --- /dev/null +++ b/tests/ember-data__serializer/README.md @@ -0,0 +1,12 @@ +# Tests for @ember-data/serializer + +This test-package provides tests for the `@ember-data/serializer` package. + +## Running Tests Locally + +If you do not already have the project cloned and dependencies installed, you may want to first read [Setting Up The Project](../../contributing/setting-up-the-project.md) + +```cli +cd tests/ember-data__serializer; +pnpm test; +``` diff --git a/tests/json-api-encapsulation/app/app.js b/tests/ember-data__serializer/app/app.js similarity index 100% rename from tests/json-api-encapsulation/app/app.js rename to tests/ember-data__serializer/app/app.js diff --git a/tests/ember-data__serializer/app/index.html b/tests/ember-data__serializer/app/index.html new file mode 100644 index 00000000000..c57859f7dfb --- /dev/null +++ b/tests/ember-data__serializer/app/index.html @@ -0,0 +1,25 @@ + + + + + + @ember-data/serializer + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/json-api-encapsulation/app/resolver.js b/tests/ember-data__serializer/app/resolver.js similarity index 100% rename from tests/json-api-encapsulation/app/resolver.js rename to tests/ember-data__serializer/app/resolver.js diff --git a/tests/model-encapsulation/app/router.js b/tests/ember-data__serializer/app/router.js similarity index 100% rename from tests/model-encapsulation/app/router.js rename to tests/ember-data__serializer/app/router.js diff --git a/tests/ember-data__serializer/app/services/store.ts b/tests/ember-data__serializer/app/services/store.ts new file mode 100644 index 00000000000..65a035fc230 --- /dev/null +++ b/tests/ember-data__serializer/app/services/store.ts @@ -0,0 +1,60 @@ +import JSONAPICache from '@ember-data/json-api'; +import { + adapterFor, + cleanup, + LegacyNetworkHandler, + normalize, + pushPayload, + serializeRecord, + serializerFor, +} from '@ember-data/legacy-compat'; +import type Model from '@ember-data/model'; +import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember-data/model/hooks'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import BaseStore, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; + +export default class Store extends BaseStore { + constructor(args: unknown) { + super(args); + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + this.requestManager.useCache(CacheHandler); + } + + createSchemaService(): SchemaService { + return buildSchema(this); + } + + override createCache(capabilities: CacheCapabilitiesManager) { + return new JSONAPICache(capabilities); + } + + override instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: Record): Model { + return instantiateRecord.call(this, identifier, createRecordArgs); + } + + override teardownRecord(record: Model) { + teardownRecord.call(this, record); + } + + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { + return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type); + } + + serializeRecord = serializeRecord; + pushPayload = pushPayload; + adapterFor = adapterFor; + serializerFor = serializerFor; + normalize = normalize; + + override destroy() { + cleanup.call(this); + super.destroy(); + } +} diff --git a/tests/request/app/styles/app.css b/tests/ember-data__serializer/app/styles/app.css similarity index 100% rename from tests/request/app/styles/app.css rename to tests/ember-data__serializer/app/styles/app.css diff --git a/tests/ember-data__serializer/app/templates/application.hbs b/tests/ember-data__serializer/app/templates/application.hbs new file mode 100644 index 00000000000..e2147cab02d --- /dev/null +++ b/tests/ember-data__serializer/app/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/tests/ember-data__serializer/config/environment.js b/tests/ember-data__serializer/config/environment.js new file mode 100644 index 00000000000..58f9db99cf0 --- /dev/null +++ b/tests/ember-data__serializer/config/environment.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-data__serializer', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/model-encapsulation/config/optional-features.json b/tests/ember-data__serializer/config/optional-features.json similarity index 100% rename from tests/model-encapsulation/config/optional-features.json rename to tests/ember-data__serializer/config/optional-features.json diff --git a/tests/model-encapsulation/config/targets.js b/tests/ember-data__serializer/config/targets.js similarity index 100% rename from tests/model-encapsulation/config/targets.js rename to tests/ember-data__serializer/config/targets.js diff --git a/tests/ember-data__serializer/ember-cli-build.js b/tests/ember-data__serializer/ember-cli-build.js new file mode 100644 index 00000000000..07003480e86 --- /dev/null +++ b/tests/ember-data__serializer/ember-cli-build.js @@ -0,0 +1,29 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, + }, + sourcemaps: { + enabled: false, + }, + }); + + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); + + return app.toTree(); +}; diff --git a/tests/ember-data__serializer/eslint.config.mjs b/tests/ember-data__serializer/eslint.config.mjs new file mode 100644 index 00000000000..e0a6b3e98f2 --- /dev/null +++ b/tests/ember-data__serializer/eslint.config.mjs @@ -0,0 +1,38 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs({ + files: ['ember-cli-build.js'], + }), + + // Test Support ================ + qunit.ember({ + allowedImports: ['@ember/object'], + }), +]; diff --git a/tests/ember-data__serializer/package.json b/tests/ember-data__serializer/package.json new file mode 100644 index 00000000000..a15b2ee512c --- /dev/null +++ b/tests/ember-data__serializer/package.json @@ -0,0 +1,118 @@ +{ + "name": "ember-data__serializer", + "version": "4.12.8", + "private": true, + "description": "Tests for the @ember-data/serializer package", + "repository": { + "type": "git", + "url": "https://github.com/emberjs/data.git", + "directory": "tests/ember-data__serializer" + }, + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", + "start": "ember serve", + "test": "ember test --test-port=0 --path=dist-test", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "@ember-data/adapter": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/optional-features": "^2.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.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-load-initializers": "^2.1.2", + "ember-maybe-import-regenerator": "^1.0.0", + "ember-qunit": "8.0.2", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", + "loader.js": "^4.7.0", + "qunit": "^2.20.1", + "qunit-dom": "^3.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" + }, + "engines": { + "node": ">= 18.20.4" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + }, + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } +} diff --git a/tests/adapter-encapsulation/public/robots.txt b/tests/ember-data__serializer/public/robots.txt similarity index 100% rename from tests/adapter-encapsulation/public/robots.txt rename to tests/ember-data__serializer/public/robots.txt diff --git a/tests/ember-data__serializer/testem.js b/tests/ember-data__serializer/testem.js new file mode 100644 index 00000000000..34bdb125e60 --- /dev/null +++ b/tests/ember-data__serializer/testem.js @@ -0,0 +1,28 @@ +const customDotReporter = require('@ember-data/unpublished-test-infra/testem/custom-dot-reporter'); + +console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); + +module.exports = { + test_page: 'tests/index.html?hidepassed&nocontainer', + disable_watching: true, + reporter: customDotReporter, + launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], + launch_in_dev: ['Chrome'], + browser_start_timeout: 120, + browser_args: { + Chrome: { + ci: [ + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900', + '--no-sandbox', + ], + }, + }, + Firefox: { + ci: ['-headless', '-width 1440', '-height 900'], + }, +}; diff --git a/tests/adapter-encapsulation/tests/unit/.gitkeep b/tests/ember-data__serializer/tests/helpers/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/tests/unit/.gitkeep rename to tests/ember-data__serializer/tests/helpers/.gitkeep diff --git a/tests/ember-data__serializer/tests/index.html b/tests/ember-data__serializer/tests/index.html new file mode 100644 index 00000000000..43fe0401eb8 --- /dev/null +++ b/tests/ember-data__serializer/tests/index.html @@ -0,0 +1,41 @@ + + + + + + @ember-data/serializer + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + +
+
+
+
+
+
+ + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + + diff --git a/tests/adapter-encapsulation/vendor/.gitkeep b/tests/ember-data__serializer/tests/integration/.gitkeep similarity index 100% rename from tests/adapter-encapsulation/vendor/.gitkeep rename to tests/ember-data__serializer/tests/integration/.gitkeep diff --git a/tests/serializer-encapsulation/tests/integration/create-record-test.js b/tests/ember-data__serializer/tests/integration/create-record-test.js similarity index 95% rename from tests/serializer-encapsulation/tests/integration/create-record-test.js rename to tests/ember-data__serializer/tests/integration/create-record-test.js index 9068d0359ac..2016df7797c 100644 --- a/tests/serializer-encapsulation/tests/integration/create-record-test.js +++ b/tests/ember-data__serializer/tests/integration/create-record-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -81,7 +80,7 @@ module('Serializer Contract | running createRecord with minimum serializer', fun class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ + return Promise.resolve({ id: '1m', type: 'person', attributes: { @@ -95,7 +94,7 @@ module('Serializer Contract | running createRecord with minimum serializer', fun const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: '1', firstName: 'John', lastName: 'Smith', @@ -176,7 +175,7 @@ module('Serializer Contract | running createRecord with minimum serializer', fun class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ + return Promise.resolve({ person: { id: '1m', type: 'person', @@ -192,7 +191,7 @@ module('Serializer Contract | running createRecord with minimum serializer', fun const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: '1', firstName: 'John', lastName: 'Smith', diff --git a/tests/serializer-encapsulation/tests/integration/delete-record-test.js b/tests/ember-data__serializer/tests/integration/delete-record-test.js similarity index 92% rename from tests/serializer-encapsulation/tests/integration/delete-record-test.js rename to tests/ember-data__serializer/tests/integration/delete-record-test.js index 0e46d859140..35d16dd248f 100644 --- a/tests/serializer-encapsulation/tests/integration/delete-record-test.js +++ b/tests/ember-data__serializer/tests/integration/delete-record-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -40,7 +39,7 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun test('save after deleting record does not call normalizeResponse and serialize', async function (assert) { let normalizeResponseCalled = 0; - let _payloads = [ + const _payloads = [ { id: '1', type: 'person', @@ -81,14 +80,14 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun _payloads = [..._payloads]; ajax(url, type) { - return resolve(this._payloads.shift()); + return Promise.resolve(this._payloads.shift()); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.deepEqual(person.toJSON(), { id: '1', @@ -109,7 +108,7 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun let serializeCalled = 0; let serializeIntoHashCalled = 0; let normalizeResponseCalled = 0; - let _payloads = [ + const _payloads = [ { id: '1', type: 'person', @@ -158,14 +157,14 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun _payloads = [..._payloads]; ajax(url, type) { - return resolve(this._payloads.shift()); + return Promise.resolve(this._payloads.shift()); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.deepEqual(person.toJSON(), { id: '1', @@ -186,7 +185,7 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun test('save after deleting record does call normalizeResponse if response provided', async function (assert) { let normalizeResponseCalled = 0; - let _payloads = [ + const _payloads = [ { id: '1', type: 'person', @@ -222,14 +221,14 @@ module('Serializer Contract | running deleteRecord with minimum serializer', fun _payloads = [..._payloads]; ajax(url, type) { - return resolve(this._payloads.shift()); + return Promise.resolve(this._payloads.shift()); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.deepEqual(person.toJSON(), { id: '1', diff --git a/tests/serializer-encapsulation/tests/integration/errors-test.js b/tests/ember-data__serializer/tests/integration/errors-test.js similarity index 89% rename from tests/serializer-encapsulation/tests/integration/errors-test.js rename to tests/ember-data__serializer/tests/integration/errors-test.js index 0d12902b761..d949ef61808 100644 --- a/tests/serializer-encapsulation/tests/integration/errors-test.js +++ b/tests/ember-data__serializer/tests/integration/errors-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { reject } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -38,7 +37,7 @@ module( test('can retrieve errors after findRecord', async function (assert) { class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return reject({ + return Promise.reject({ errors: [ { status: '404', @@ -50,7 +49,7 @@ module( } this.owner.register('adapter:application', TestAdapter); - let store = this.owner.lookup('service:Store'); + const store = this.owner.lookup('service:Store'); let errors; try { await store.findRecord('person', 1); @@ -67,7 +66,7 @@ module( test('can retrieve errors after save', async function (assert) { class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return reject({ + return Promise.reject({ errors: [ { status: '400', @@ -85,8 +84,8 @@ module( } this.owner.register('adapter:application', TestAdapter); - let store = this.owner.lookup('service:Store'); - let person = store.createRecord('person', {}); + const store = this.owner.lookup('service:Store'); + const person = store.createRecord('person', {}); let errors; try { await person.save(); diff --git a/tests/serializer-encapsulation/tests/integration/normalize-test.js b/tests/ember-data__serializer/tests/integration/normalize-test.js similarity index 95% rename from tests/serializer-encapsulation/tests/integration/normalize-test.js rename to tests/ember-data__serializer/tests/integration/normalize-test.js index 1ad8db1fcac..2b6c2469b7a 100644 --- a/tests/serializer-encapsulation/tests/integration/normalize-test.js +++ b/tests/ember-data__serializer/tests/integration/normalize-test.js @@ -1,8 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; @@ -54,7 +54,7 @@ module('Serializer Contract | normalize method forwards to Serializer#normalize' const store = this.owner.lookup('service:store'); - let payload = store.normalize('person', { + const payload = store.normalize('person', { id: '1', type: 'person', attributes: { diff --git a/tests/serializer-encapsulation/tests/integration/push-payload-test.js b/tests/ember-data__serializer/tests/integration/push-payload-test.js similarity index 90% rename from tests/serializer-encapsulation/tests/integration/push-payload-test.js rename to tests/ember-data__serializer/tests/integration/push-payload-test.js index 5c4696b5780..ae4514d4259 100644 --- a/tests/serializer-encapsulation/tests/integration/push-payload-test.js +++ b/tests/ember-data__serializer/tests/integration/push-payload-test.js @@ -1,8 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; @@ -70,7 +70,7 @@ module('Serializer Contract | pushPayload method forwards to Serializer#pushPayl lastName: 'Smith', }, }); - let person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); assert.strictEqual(pushPayloadCalled, 1, 'pushPayload called once'); assert.deepEqual( @@ -95,7 +95,7 @@ module('Serializer Contract | pushPayload method forwards to Serializer#pushPayl const store = this.owner.lookup('service:store'); - assert.throws(() => { + await assert.expectAssertion(() => { store.pushPayload('person', { data: { id: '1', @@ -106,7 +106,7 @@ module('Serializer Contract | pushPayload method forwards to Serializer#pushPayl }, }, }); - }, /You must define a pushPayload method in your serializer in order to call store.pushPayload/); + }, `You cannot use 'store.pushPayload(, )' unless the serializer for 'person' defines 'pushPayload'`); } ); }); diff --git a/tests/serializer-encapsulation/tests/integration/relationships-test.js b/tests/ember-data__serializer/tests/integration/relationships-test.js similarity index 94% rename from tests/serializer-encapsulation/tests/integration/relationships-test.js rename to tests/ember-data__serializer/tests/integration/relationships-test.js index 42b46349450..6fb05e64f19 100644 --- a/tests/serializer-encapsulation/tests/integration/relationships-test.js +++ b/tests/ember-data__serializer/tests/integration/relationships-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -84,14 +83,14 @@ module('Serializer Contract | running requests for async relationships with mini coalesceFindRequests = true; ajax(url, type) { - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let post = store.push({ + const post = store.push({ data: { id: '1', type: 'post', @@ -114,7 +113,7 @@ module('Serializer Contract | running requests for async relationships with mini }, }, }); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual( @@ -158,14 +157,14 @@ module('Serializer Contract | running requests for async relationships with mini coalesceFindRequests = true; ajax(url, type) { - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let post = store.push({ + const post = store.push({ data: { id: '1', type: 'post', @@ -181,7 +180,7 @@ module('Serializer Contract | running requests for async relationships with mini }, }, }); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual( @@ -224,7 +223,7 @@ module('Serializer Contract | running requests for async relationships with mini coalesceFindRequests = true; ajax(url, type) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -239,7 +238,7 @@ module('Serializer Contract | running requests for async relationships with mini const store = this.owner.lookup('service:store'); - let comment = store.push({ + const comment = store.push({ data: { id: '1', type: 'comment', @@ -255,7 +254,7 @@ module('Serializer Contract | running requests for async relationships with mini }, }, }); - let post = await comment.post; + const post = await comment.post; assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual(post.title, 'Chris', 'response is expected response'); diff --git a/tests/serializer-encapsulation/tests/integration/requests-test.js b/tests/ember-data__serializer/tests/integration/requests-test.js similarity index 92% rename from tests/serializer-encapsulation/tests/integration/requests-test.js rename to tests/ember-data__serializer/tests/integration/requests-test.js index 3b4faffe056..bb6dd16ac5d 100644 --- a/tests/serializer-encapsulation/tests/integration/requests-test.js +++ b/tests/ember-data__serializer/tests/integration/requests-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -47,14 +46,14 @@ module('Serializer Contract | running requests with minimum serializer', functio class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let response = await store.findAll('person'); + const response = await store.findAll('person'); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual( @@ -95,7 +94,7 @@ module('Serializer Contract | running requests with minimum serializer', functio class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: 'urn:person:1', @@ -110,7 +109,7 @@ module('Serializer Contract | running requests with minimum serializer', functio const store = this.owner.lookup('service:store'); - let response = await store.findRecord('person', 'urn:person:1'); + const response = await store.findRecord('person', 'urn:person:1'); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual(response.name, 'John', 'response is expected response'); @@ -141,14 +140,14 @@ module('Serializer Contract | running requests with minimum serializer', functio class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let response = await store.query('person', { name: 'Chris' }); + const response = await store.query('person', { name: 'Chris' }); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual( @@ -189,7 +188,7 @@ module('Serializer Contract | running requests with minimum serializer', functio class TestAdapter extends JSONAPIAdapter { ajax(url, type) { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: 'urn:person:1', @@ -204,7 +203,7 @@ module('Serializer Contract | running requests with minimum serializer', functio const store = this.owner.lookup('service:store'); - let response = await store.queryRecord('person', { name: 'Chris' }); + const response = await store.queryRecord('person', { name: 'Chris' }); assert.strictEqual(normalizeResponseCalled, 1, 'normalizeResponse is called once'); assert.deepEqual(response.name, 'John', 'response is expected response'); diff --git a/tests/serializer-encapsulation/tests/integration/serialize-test.js b/tests/ember-data__serializer/tests/integration/serialize-test.js similarity index 92% rename from tests/serializer-encapsulation/tests/integration/serialize-test.js rename to tests/ember-data__serializer/tests/integration/serialize-test.js index 96b8c75573f..6a2fd6080f0 100644 --- a/tests/serializer-encapsulation/tests/integration/serialize-test.js +++ b/tests/ember-data__serializer/tests/integration/serialize-test.js @@ -1,8 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; @@ -63,7 +63,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: '1', firstName: 'John', lastName: 'Smith', @@ -78,7 +78,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' }, }); - let serializedPerson = person.serialize(); + const serializedPerson = person.serialize(); assert.strictEqual(serializeCalled, 1, 'serialize called once'); assert.deepEqual(serializedPerson, { @@ -118,7 +118,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: '1', firstName: 'John', lastName: 'Smith', @@ -133,7 +133,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' }, }); - let serializedPerson = person._createSnapshot().serialize(); + const serializedPerson = person._createSnapshot().serialize(); assert.strictEqual(serializeCalled, 1, 'serialize called once'); assert.deepEqual(serializedPerson, { @@ -173,7 +173,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: '1', firstName: 'John', lastName: 'Smith', @@ -188,7 +188,7 @@ module('Serializer Contract | serialize methods forward to Serializer#serialize' }, }); - let serializedPerson = store.serializeRecord(person); + const serializedPerson = store.serializeRecord(person); assert.strictEqual(serializeCalled, 1, 'serialize called once'); assert.deepEqual(serializedPerson, { diff --git a/tests/serializer-encapsulation/tests/integration/update-record-test.js b/tests/ember-data__serializer/tests/integration/update-record-test.js similarity index 94% rename from tests/serializer-encapsulation/tests/integration/update-record-test.js rename to tests/ember-data__serializer/tests/integration/update-record-test.js index 24df0a53c39..d03e41f36c4 100644 --- a/tests/serializer-encapsulation/tests/integration/update-record-test.js +++ b/tests/ember-data__serializer/tests/integration/update-record-test.js @@ -1,9 +1,8 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import Store from 'serializer-encapsulation-test-app/services/store'; +import Store from 'ember-data__serializer/services/store'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -41,7 +40,7 @@ module('Serializer Contract | running createRecord [update] with minimum seriali test('save after mutating record calls normalizeResponse and serialize', async function (assert) { let serializeCalled = 0; let normalizeResponseCalled = 0; - let _payloads = [ + const _payloads = [ { id: '1', type: 'person', @@ -98,14 +97,14 @@ module('Serializer Contract | running createRecord [update] with minimum seriali _payloads = [..._payloads]; ajax(url, type) { - return resolve(this._payloads.shift()); + return Promise.resolve(this._payloads.shift()); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.deepEqual(person.toJSON(), { id: '1', @@ -138,7 +137,7 @@ module('Serializer Contract | running createRecord [update] with minimum seriali let serializeIntoHashCalled = 0; let normalizeResponseCalled = 0; - let _payloads = [ + const _payloads = [ { id: '1', type: 'person', @@ -201,14 +200,14 @@ module('Serializer Contract | running createRecord [update] with minimum seriali _payloads = [..._payloads]; ajax(url, type) { - return resolve(this._payloads.shift()); + return Promise.resolve(this._payloads.shift()); } } this.owner.register('adapter:application', TestAdapter); const store = this.owner.lookup('service:store'); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.deepEqual(person.toJSON(), { id: '1', diff --git a/tests/ember-data__serializer/tests/test-helper.js b/tests/ember-data__serializer/tests/test-helper.js new file mode 100644 index 00000000000..782af4b5e07 --- /dev/null +++ b/tests/ember-data__serializer/tests/test-helper.js @@ -0,0 +1,19 @@ +import { setApplication } from '@ember/test-helpers'; + +import * as QUnit from 'qunit'; +import { setup } from 'qunit-dom'; + +import { start } from 'ember-qunit'; + +import configureAsserts from '@ember-data/unpublished-test-infra/test-support/asserts/index'; + +import Application from '../app'; +import config from '../config/environment'; + +setup(QUnit.assert); +configureAsserts(QUnit.hooks); + +setApplication(Application.create(config.APP)); + +QUnit.config.testTimeout = 2000; +start({ setupTestIsolationValidation: true }); diff --git a/tests/debug-encapsulation/app/components/.gitkeep b/tests/ember-data__serializer/tests/unit/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/components/.gitkeep rename to tests/ember-data__serializer/tests/unit/.gitkeep diff --git a/tests/ember-data__serializer/tsconfig.json b/tests/ember-data__serializer/tsconfig.json new file mode 100644 index 00000000000..ddaf75c71b7 --- /dev/null +++ b/tests/ember-data__serializer/tsconfig.json @@ -0,0 +1,94 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/adapter": ["../../packages/adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../../packages/adapter/unstable-preview-types/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/adapter" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/debug-encapsulation/app/controllers/.gitkeep b/tests/ember-data__serializer/vendor/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/controllers/.gitkeep rename to tests/ember-data__serializer/vendor/.gitkeep diff --git a/tests/embroider-basic-compat/README.md b/tests/embroider-basic-compat/README.md index eba7de52fdb..b7b0252c9f1 100644 --- a/tests/embroider-basic-compat/README.md +++ b/tests/embroider-basic-compat/README.md @@ -33,12 +33,6 @@ Make use of the many generators for code, try `ember help generate` for more det * `ember test` * `ember test --server` -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - ### Building * `ember build` (development) diff --git a/tests/embroider-basic-compat/app/adapters/application.ts b/tests/embroider-basic-compat/app/adapters/application.ts new file mode 100644 index 00000000000..091f5ab1225 --- /dev/null +++ b/tests/embroider-basic-compat/app/adapters/application.ts @@ -0,0 +1,14 @@ +import RESTAdapter from '@ember-data/adapter/rest'; +import type { SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; + +export default class ApplicationAdapter extends RESTAdapter { + namespace = 'api'; + + urlForFindAll(type: string, snapshots: SnapshotRecordArray) { + let url = super.urlForFindAll(type, snapshots); + if (url.endsWith('/')) { + url = url.substring(0, url.length - 2); + } + return url + '.json'; + } +} diff --git a/tests/embroider-basic-compat/app/templates/application.hbs b/tests/embroider-basic-compat/app/templates/application.hbs index eaf8670b247..e2147cab02d 100644 --- a/tests/embroider-basic-compat/app/templates/application.hbs +++ b/tests/embroider-basic-compat/app/templates/application.hbs @@ -1,7 +1 @@ -
-

Ember Data Embroider Compat Test

- - {{outlet}} - - Tests -
+{{outlet}} \ No newline at end of file diff --git a/tests/embroider-basic-compat/config/environment.js b/tests/embroider-basic-compat/config/environment.js index 1639c79a70d..546974efdb3 100644 --- a/tests/embroider-basic-compat/config/environment.js +++ b/tests/embroider-basic-compat/config/environment.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = function (environment) { - let ENV = { + const ENV = { modulePrefix: 'embroider-basic-compat', environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build diff --git a/tests/embroider-basic-compat/ember-cli-build.js b/tests/embroider-basic-compat/ember-cli-build.js index be6dc70867e..503544fb6f5 100644 --- a/tests/embroider-basic-compat/ember-cli-build.js +++ b/tests/embroider-basic-compat/ember-cli-build.js @@ -1,29 +1,27 @@ -/* eslint node/no-unpublished-require: 'off' */ - 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + const plugins = macros(); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins, + }, 'ember-cli-babel': { + throwUnlessParallelizable: true, enableTypeScriptTransform: true, }, }); - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); const { Webpack } = require('@embroider/webpack'); return require('@embroider/compat').compatBuild(app, Webpack, { @@ -33,25 +31,21 @@ module.exports = function (defaults) { }, ], compatAdapters: new Map([ - ['@ember-data/store', null], - ['@ember-data/record-data', null], - ['@ember-data/serializer', null], + ['@ember-data/active-record', null], ['@ember-data/adapter', null], - ['@ember-data/model', null], ['@ember-data/debug', null], - ['@ember-data/tracking', null], + ['@ember-data/graph', null], + ['@ember-data/json-api', null], + ['@ember-data/legacy-compat', null], + ['@ember-data/model', null], + ['@ember-data/record-data', null], + ['@ember-data/request-utils', null], ['@ember-data/request', null], + ['@ember-data/rest', null], + ['@ember-data/serializer', null], + ['@ember-data/store', null], + ['@ember-data/tracking', null], ['ember-data', null], ]), - packageRules: [ - { - package: '@ember-data/store', - addonModules: { - '-private.js': { - dependsOnModules: ['@ember-data/json-api'], - }, - }, - }, - ], }); }; diff --git a/tests/embroider-basic-compat/eslint.config.mjs b/tests/embroider-basic-compat/eslint.config.mjs new file mode 100644 index 00000000000..c9815c47c47 --- /dev/null +++ b/tests/embroider-basic-compat/eslint.config.mjs @@ -0,0 +1,32 @@ +// @ts-check +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 * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/application'], + rules: { + // TODO: Enable these once we get types working properly + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + qunit.ember(), +]; diff --git a/tests/embroider-basic-compat/package.json b/tests/embroider-basic-compat/package.json index 9a1e98d69ba..0321496dc43 100644 --- a/tests/embroider-basic-compat/package.json +++ b/tests/embroider-basic-compat/package.json @@ -16,64 +16,112 @@ }, "scripts": { "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependencies": { - "ember-auto-import": "^2.6.1", - "ember-data": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "ember-inflector": "^4.0.2" + "test:embroider": "ember test --test-port=0", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, + "dependencies": {}, "dependenciesMeta": { "ember-data": { "injected": true }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/adapter": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/serializer": { + "injected": true + }, "@ember-data/unpublished-test-infra": { "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.9.3", - "@embroider/compat": "^2.1.1", - "@embroider/core": "^2.1.1", - "@embroider/webpack": "^2.1.1", + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^3.1.1", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.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", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "@types/ember": "^4.0.3", - "@types/ember-qunit": "^5.0.2", - "@types/ember-testing-helpers": "^0.0.4", - "@types/rsvp": "^4.0.4", - "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-fastboot": "^4.1.0", - "ember-cli-fastboot-testing": "^0.6.0", - "ember-cli-htmlbars": "^6.2.0", + "@ember-data/request": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/build-config": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-fastboot": "^4.1.5", + "ember-cli-fastboot-testing": "^0.6.2", + "ember-cli-htmlbars": "^6.3.0", "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", + "ember-inflector": "4.0.3", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-simple-tree": "^0.8.3", - "ember-source": "~4.12.0", + "ember-qunit": "8.0.2", + "ember-resolver": "^11.0.1", + "ember-simple-tree": "^0.8.4", + "ember-source": "~5.12.0", "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "typescript": "~5.0.3", - "webpack": "^5.77.0" + "qunit": "^2.20.1", + "qunit-dom": "^3.1.1", + "typescript": "^5.4.5" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "ember": { "edition": "octane" @@ -85,5 +133,5 @@ "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/tests/embroider-basic-compat/testem.js b/tests/embroider-basic-compat/testem.js index e10b064501a..34bdb125e60 100644 --- a/tests/embroider-basic-compat/testem.js +++ b/tests/embroider-basic-compat/testem.js @@ -1,7 +1,5 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); +const customDotReporter = require('@ember-data/unpublished-test-infra/testem/custom-dot-reporter'); -// eslint-disable-next-line no-console console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); module.exports = { diff --git a/tests/embroider-basic-compat/tsconfig.json b/tests/embroider-basic-compat/tsconfig.json new file mode 100644 index 00000000000..4d54bd9854f --- /dev/null +++ b/tests/embroider-basic-compat/tsconfig.json @@ -0,0 +1,104 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/unstable-preview-types/*"], + "ember-data": ["../../packages/-ember-data/unstable-preview-types"], + "ember-data/*": ["../../packages/-ember-data/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/adapter": ["../../packages/adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../../packages/adapter/unstable-preview-types/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../../packages/serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../../packages/serializer/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/-ember-data" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/adapter" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/serializer" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/fastboot/.ember-cli b/tests/fastboot/.ember-cli deleted file mode 100644 index ee64cfed2a8..00000000000 --- a/tests/fastboot/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/tests/fastboot/.gitignore b/tests/fastboot/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/fastboot/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/fastboot/README.md b/tests/fastboot/README.md index aec74524d20..a816b014521 100644 --- a/tests/fastboot/README.md +++ b/tests/fastboot/README.md @@ -33,12 +33,6 @@ Make use of the many generators for code, try `ember help generate` for more det * `ember test` * `ember test --server` -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - ### Building * `ember build` (development) diff --git a/tests/fastboot/app/adapters/application.ts b/tests/fastboot/app/adapters/application.ts index 091f5ab1225..3420114fc41 100644 --- a/tests/fastboot/app/adapters/application.ts +++ b/tests/fastboot/app/adapters/application.ts @@ -1,3 +1,4 @@ +// @ts-expect-error import RESTAdapter from '@ember-data/adapter/rest'; import type { SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; diff --git a/tests/fastboot/app/index.html b/tests/fastboot/app/index.html index b05f193b1f8..46c6bd33b4c 100644 --- a/tests/fastboot/app/index.html +++ b/tests/fastboot/app/index.html @@ -3,7 +3,7 @@ - Ember Data + EmberData diff --git a/tests/fastboot/app/models/person.js b/tests/fastboot/app/models/person.js deleted file mode 100644 index 8255ebebbe3..00000000000 --- a/tests/fastboot/app/models/person.js +++ /dev/null @@ -1,21 +0,0 @@ -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - -export default class Person extends Model { - @attr() - name; - - @hasMany('person', { async: true, inverse: 'parent' }) - children; - - @belongsTo('person', { async: true, inverse: 'children' }) - parent; - - get parentId() { - return this.belongsTo('parent').id(); - } - - toNode() { - const { id, name, parentId } = this; - return { id, name, parentId }; - } -} diff --git a/tests/fastboot/app/models/person.ts b/tests/fastboot/app/models/person.ts new file mode 100644 index 00000000000..e0d42f1af43 --- /dev/null +++ b/tests/fastboot/app/models/person.ts @@ -0,0 +1,25 @@ +import type { AsyncBelongsTo, AsyncHasMany } from '@ember-data/model'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type { Type } from '@warp-drive/core-types/symbols'; + +export default class Person extends Model { + @attr() + declare name: string; + + @hasMany('person', { async: true, inverse: 'parent' }) + declare children: AsyncHasMany; + + @belongsTo('person', { async: true, inverse: 'children' }) + declare parent: AsyncBelongsTo; + + get parentId(): string | null { + return this.belongsTo('parent').id(); + } + + toNode() { + const { id, name, parentId } = this; + return { id, name, parentId }; + } + + declare [Type]: 'person'; +} diff --git a/tests/fastboot/app/routes/index.js b/tests/fastboot/app/routes/index.js deleted file mode 100644 index e2f49b7326d..00000000000 --- a/tests/fastboot/app/routes/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import Route from '@ember/routing/route'; -import { inject as service } from '@ember/service'; - -import { buildTree } from 'ember-simple-tree/utils/tree'; - -export default class IndexRoute extends Route { - @service store; - - async model() { - const people = await this.store.findAll('person'); - const tree = buildTree(people.map((person) => person.toNode())); - return tree; - } -} diff --git a/tests/fastboot/app/routes/index.ts b/tests/fastboot/app/routes/index.ts new file mode 100644 index 00000000000..0666a7f7452 --- /dev/null +++ b/tests/fastboot/app/routes/index.ts @@ -0,0 +1,20 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +// @ts-expect-error untyped +import { buildTree } from 'ember-simple-tree/utils/tree'; + +import type Person from '../models/person'; +import type Store from '../services/store'; + +export default class IndexRoute extends Route { + @service declare store: Store; + + override async model() { + const people = await this.store.findAll('person'); + const tree = buildTree(people.map((person) => person.toNode())); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return tree; + } +} diff --git a/tests/fastboot/app/serializers/application.ts b/tests/fastboot/app/serializers/application.ts index 2129eaeaeef..0bcb026f0e5 100644 --- a/tests/fastboot/app/serializers/application.ts +++ b/tests/fastboot/app/serializers/application.ts @@ -1,4 +1,6 @@ +// @ts-expect-error import JSONSerializer from '@ember-data/serializer/json'; +// @ts-expect-error import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; export default JSONSerializer.extend(EmbeddedRecordsMixin, {}); diff --git a/tests/main/app/services/store.ts b/tests/fastboot/app/services/store.ts similarity index 100% rename from tests/main/app/services/store.ts rename to tests/fastboot/app/services/store.ts diff --git a/tests/fastboot/app/templates/application.hbs b/tests/fastboot/app/templates/application.hbs index 7f4fe2e3a21..fef9405a20c 100644 --- a/tests/fastboot/app/templates/application.hbs +++ b/tests/fastboot/app/templates/application.hbs @@ -1,7 +1,2 @@ -
-

Ember Data

- - {{outlet}} - - Tests -
+

EmberData Fastboot Tests

+{{outlet}} \ No newline at end of file diff --git a/tests/fastboot/config/environment.js b/tests/fastboot/config/environment.js index 0bb53e4b82f..4ad322afa77 100644 --- a/tests/fastboot/config/environment.js +++ b/tests/fastboot/config/environment.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = function (environment) { - let ENV = { + const ENV = { modulePrefix: 'fastboot-test-app', environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build diff --git a/tests/fastboot/config/fastboot-testing.js b/tests/fastboot/config/fastboot-testing.js index 2049fbc305d..4e0b4d5a879 100644 --- a/tests/fastboot/config/fastboot-testing.js +++ b/tests/fastboot/config/fastboot-testing.js @@ -1,15 +1 @@ -/* global ReadableStream, WritableStream, TransformStream */ -module.exports = { - buildSandboxGlobals(defaultGlobals) { - return Object.assign({}, defaultGlobals, { - AbortController, - ReadableStream: - typeof ReadableStream !== 'undefined' ? ReadableStream : require('node:stream/web').ReadableStream, - WritableStream: - typeof WritableStream !== 'undefined' ? WritableStream : require('node:stream/web').WritableStream, - TransformStream: - typeof TransformStream !== 'undefined' ? TransformStream : require('node:stream/web').TransformStream, - Headers: typeof Headers !== 'undefined' ? Headers : undefined, - }); - }, -}; +module.exports = require('./fastboot'); diff --git a/tests/fastboot/config/fastboot.js b/tests/fastboot/config/fastboot.js index 02ec572eb6b..ce797e71f04 100644 --- a/tests/fastboot/config/fastboot.js +++ b/tests/fastboot/config/fastboot.js @@ -1,4 +1,4 @@ -/* global ReadableStream, WritableStream, TransformStream */ +/* eslint-disable n/no-unsupported-features/node-builtins */ module.exports = function (environment) { return { buildSandboxGlobals(defaultGlobals) { diff --git a/tests/fastboot/ember-cli-build.js b/tests/fastboot/ember-cli-build.js index 4a994397055..b4f794466fe 100644 --- a/tests/fastboot/ember-cli-build.js +++ b/tests/fastboot/ember-cli-build.js @@ -1,29 +1,26 @@ -/* eslint node/no-unpublished-require: 'off' */ - 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + + const app = new EmberApp(defaults, { + babel: { + // this ensures that the same build-time code stripping that is done + // for library packages is also done for our tests and dummy app + plugins: [...macros()], + }, 'ember-cli-babel': { + throwUnlessParallelizable: true, enableTypeScriptTransform: true, }, }); - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + }); return app.toTree(); }; diff --git a/tests/fastboot/eslint.config.mjs b/tests/fastboot/eslint.config.mjs new file mode 100644 index 00000000000..b085eaadcf2 --- /dev/null +++ b/tests/fastboot/eslint.config.mjs @@ -0,0 +1,44 @@ +// @ts-check +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 * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/routing/route', '@ember/application', '@ember/service'], + }), + + // browser (js/ts) ================ + typescript.browser({ + files: ['**/*.ts', '**/*.gts'], + srcDirs: ['app', 'tests'], + allowedImports: ['@ember/routing/route', '@ember/application', '@ember/service'], + rules: { + // TODO: Enable these once we get types working properly + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs({ + files: ['config/fastboot.js', 'config/fastboot-testing.js'], + }), + + // Test Support ================ + qunit.ember({ + allowedImports: ['@ember/object'], + }), +]; diff --git a/tests/fastboot/package.json b/tests/fastboot/package.json index 29121efe9b6..f5222e0a193 100644 --- a/tests/fastboot/package.json +++ b/tests/fastboot/package.json @@ -16,17 +16,18 @@ }, "scripts": { "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "tsc --noEmit", "start": "ember serve", - "test": "ember test --test-port=0" + "test:fastboot": "ember test --test-port=0", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependencies": { - "ember-auto-import": "^2.6.1", - "ember-data": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/string": "^3.0.1 || ^4.0.0", - "ember-inflector": "^4.0.2" + "@ember-data/unpublished-test-infra": "workspace:*", + "ember-auto-import": "^2.8.1", + "ember-data": "workspace:*", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "webpack": "^5.92.0" }, "dependenciesMeta": { "ember-data": { @@ -34,43 +35,72 @@ }, "@ember-data/unpublished-test-infra": { "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.9.3", + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^3.1.1", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "@types/ember": "^4.0.3", - "@types/ember-qunit": "^5.0.2", - "@types/ember-testing-helpers": "^0.0.4", - "@types/rsvp": "^4.0.4", - "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~4.11.0", - "ember-cli-version-checker": "^5.1.2", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-fastboot": "^4.1.0", - "ember-cli-fastboot-testing": "^0.6.0", - "ember-cli-htmlbars": "^6.2.0", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@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", + "ember-cli-fastboot": "^4.1.5", + "ember-cli-fastboot-testing": "^0.6.2", + "ember-cli-htmlbars": "^6.3.0", "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-version-checker": "^5.1.2", + "ember-inflector": "4.0.3", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-simple-tree": "^0.8.3", - "ember-source": "~4.12.0", + "ember-qunit": "8.0.2", + "ember-resolver": "^11.0.1", + "ember-simple-tree": "^0.8.4", + "ember-source": "~5.12.0", "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "typescript": "~5.0.3", - "webpack": "^5.77.0" + "qunit": "^2.20.1", + "qunit-dom": "^3.1.1", + "typescript": "^5.4.5" }, "engines": { - "node": "16.* || >= 18.*" + "node": ">= 22" }, "ember": { "edition": "octane" @@ -82,5 +112,5 @@ "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/tests/fastboot/public/api/people.json b/tests/fastboot/public/api/people.json index 066c1f0abc1..c936e321509 100644 --- a/tests/fastboot/public/api/people.json +++ b/tests/fastboot/public/api/people.json @@ -29,4 +29,4 @@ "children": [], "parent": "3:has-2-children-and-parent" } -] \ No newline at end of file +] diff --git a/tests/fastboot/testem.js b/tests/fastboot/testem.js index e10b064501a..34bdb125e60 100644 --- a/tests/fastboot/testem.js +++ b/tests/fastboot/testem.js @@ -1,7 +1,5 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); +const customDotReporter = require('@ember-data/unpublished-test-infra/testem/custom-dot-reporter'); -// eslint-disable-next-line no-console console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); module.exports = { diff --git a/tests/fastboot/tests/fastboot/index-test.js b/tests/fastboot/tests/fastboot/index-test.js index d9188650ef3..3667dbbbf0b 100644 --- a/tests/fastboot/tests/fastboot/index-test.js +++ b/tests/fastboot/tests/fastboot/index-test.js @@ -11,8 +11,7 @@ module('Browser | index', function (hooks) { test('(browser) it renders a page...', async function (assert) { await visit('/'); - assert.dom('h1').hasText('Ember Data'); - assert.dom('a').hasAttribute('href', '/tests'); + assert.dom('h1').hasText('EmberData Fastboot Tests'); assert.dom('ul').exists(); assert.dom('ul>li').isVisible({ count: 5 }); @@ -27,8 +26,7 @@ module('FastBoot | index', function (hooks) { test('(FastBoot) it renders a page...', async function (assert) { await SSR('/'); - assert.dom('h1').hasText('Ember Data'); - assert.dom('a').hasAttribute('href', '/tests'); + assert.dom('h1').hasText('EmberData Fastboot Tests'); assert.dom('ul').exists(); assert.dom('ul>li').isVisible({ count: 5 }); diff --git a/tests/fastboot/tests/fastboot/person/new-test.js b/tests/fastboot/tests/fastboot/person/new-test.js index 0f3df0a7629..13623dd4e20 100644 --- a/tests/fastboot/tests/fastboot/person/new-test.js +++ b/tests/fastboot/tests/fastboot/person/new-test.js @@ -12,8 +12,7 @@ module('Browser | /person/new', function (hooks) { await visit('/person/new'); // from application.hbs - assert.dom('h1').hasText('Ember Data'); - assert.dom('a').hasAttribute('href', '/tests'); + assert.dom('h1').hasText('EmberData Fastboot Tests'); assert.dom('.person-name').exists(); }); @@ -26,8 +25,7 @@ module('FastBoot | /person/new', function (hooks) { await SSR('/person/new'); // from application.hbs - assert.dom('h1').hasText('Ember Data'); - assert.dom('a').hasAttribute('href', '/tests'); + assert.dom('h1').hasText('EmberData Fastboot Tests'); assert.dom('.person-name').exists(); }); diff --git a/tests/fastboot/tsconfig.json b/tests/fastboot/tsconfig.json new file mode 100644 index 00000000000..40aaf336821 --- /dev/null +++ b/tests/fastboot/tsconfig.json @@ -0,0 +1,79 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*", "types/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/unstable-preview-types/*"], + "ember-data": ["../../packages/-ember-data/unstable-preview-types"], + "ember-data/*": ["../../packages/-ember-data/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/-ember-data" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/request-utils" + } + ] +} diff --git a/tests/fastboot/types/fastboot-test-app/index.d.ts b/tests/fastboot/types/fastboot-test-app/index.d.ts deleted file mode 100644 index 74e16745404..00000000000 --- a/tests/fastboot/types/fastboot-test-app/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Ember from 'ember'; - -declare global { - interface Array extends Ember.ArrayPrototypeExtensions {} - // interface Function extends Ember.FunctionPrototypeExtensions {} -} - -export {}; diff --git a/tests/fastboot/types/global.d.ts b/tests/fastboot/types/global.d.ts index d5302315f6b..6ee18a97caf 100644 --- a/tests/fastboot/types/global.d.ts +++ b/tests/fastboot/types/global.d.ts @@ -1,7 +1,2 @@ // Types for compiled templates -declare module 'fastboot-test-app/templates/*' { - import type { TemplateFactory } from 'htmlbars-inline-precompile'; - - const tmpl: TemplateFactory; - export default tmpl; -} +declare module 'fastboot-test-app/templates/*' {} diff --git a/tests/full-data-asset-size-app/config/environment.js b/tests/full-data-asset-size-app/config/environment.js index 2352d0e4bd7..edc2e75993e 100644 --- a/tests/full-data-asset-size-app/config/environment.js +++ b/tests/full-data-asset-size-app/config/environment.js @@ -5,7 +5,7 @@ module.exports = function (environment) { modulePrefix: 'full-data-asset-size-app', environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { FEATURES: {}, EXTEND_PROTOTYPES: {}, diff --git a/tests/full-data-asset-size-app/ember-cli-build.js b/tests/full-data-asset-size-app/ember-cli-build.js index 06d52ab1735..7da2fae3da6 100644 --- a/tests/full-data-asset-size-app/ember-cli-build.js +++ b/tests/full-data-asset-size-app/ember-cli-build.js @@ -1,9 +1,10 @@ -/* eslint-disable node/no-unpublished-require */ 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = function (defaults) { +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); const terserSettings = { enabled: true, exclude: ['assets/main-test-app.js', 'assets/tests.js', 'assets/test-support.js'], @@ -31,39 +32,28 @@ module.exports = function (defaults) { }, }; - let config = { - compatWith: '99', - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }; - let app = new EmberApp(defaults, { - emberData: config, + const app = new EmberApp(defaults, { babel: { // this ensures that the same build-time code stripping that is done // for library packages is also done for our tests and dummy app - plugins: [...require('@ember-data/private-build-infra/src/debug-macros')(config)], - }, - 'ember-cli-babel': { - throwUnlessParallelizable: true, - includeExternalHelpers: true, + plugins: [...macros()], }, fingerprint: { enabled: false, }, 'ember-cli-terser': terserSettings, - '@embroider/macros': { - setConfig: { - '@ember-data/store': { - polyfillUUID: false, - }, - }, + 'ember-cli-babel': { + throwUnlessParallelizable: true, + enableTypeScriptTransform: true, }, sourcemaps: { enabled: false, }, }); + setConfig(app, __dirname, { + compatWith: '99.0', + }); + return app.toTree(); }; diff --git a/tests/full-data-asset-size-app/package.json b/tests/full-data-asset-size-app/package.json index d1cb66062ab..81fe1752474 100644 --- a/tests/full-data-asset-size-app/package.json +++ b/tests/full-data-asset-size-app/package.json @@ -16,50 +16,47 @@ }, "scripts": { "build": "ember build", - "start": "ember serve" + "start": "ember serve", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependenciesMeta": { "ember-data": { "injected": true - }, - "@ember-data/private-build-infra": { - "injected": true } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember/optional-features": "^2.1.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.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", + "ember-cli-htmlbars": "^6.3.0", "ember-cli-terser": "^4.0.2", - "ember-data": "workspace:4.12.8", - "@ember/string": "^4.0.0", - "ember-export-application-global": "^2.0.1", + "ember-data": "workspace:*", + "@ember/test-waiters": "^3.1.0", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", "loader.js": "^4.7.0", - "webpack": "^5.77.0", + "webpack": "^5.92.0", "zlib": "1.0.5" }, "ember": { "edition": "octane" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } } diff --git a/tests/graph/README.md b/tests/graph/README.md deleted file mode 100644 index 9b5d77617a2..00000000000 --- a/tests/graph/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# graph-tests - -Provides testing for the Cache's Graph storage diff --git a/tests/graph/app/app.ts b/tests/graph/app/app.ts deleted file mode 100644 index 1f39476ae86..00000000000 --- a/tests/graph/app/app.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -class App extends Application { - modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; - Resolver = Resolver; -} - -loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/tests/graph/app/index.html b/tests/graph/app/index.html deleted file mode 100644 index 1cb66ddd05c..00000000000 --- a/tests/graph/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EmberData Graph Test App - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/graph/app/services/store.ts b/tests/graph/app/services/store.ts deleted file mode 100644 index 5b9cb27cfc6..00000000000 --- a/tests/graph/app/services/store.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; -import RequestManager from '@ember-data/request'; -import Fetch from '@ember-data/request/fetch'; -import BaseStore, { CacheHandler } from '@ember-data/store'; - -export default class Store extends BaseStore { - constructor(args: Record) { - super(args); - this.requestManager = new RequestManager(); - this.requestManager.use([LegacyNetworkHandler, Fetch]); - this.requestManager.useCache(CacheHandler); - } -} diff --git a/tests/graph/app/templates/.gitkeep b/tests/graph/app/templates/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/graph/config/environment.js b/tests/graph/config/environment.js deleted file mode 100644 index fdfbc8a6fdc..00000000000 --- a/tests/graph/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'graph-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/graph/ember-cli-build.js b/tests/graph/ember-cli-build.js deleted file mode 100644 index 2fa1e887d48..00000000000 --- a/tests/graph/ember-cli-build.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - const compatWith = process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null; - let app = new EmberApp(defaults, { - emberData: { - compatWith, - }, - babel: { - // this ensures that the same build-time code stripping that is done - // for library packages is also done for our tests and dummy app - plugins: [ - ...require('@ember-data/private-build-infra/src/debug-macros')({ - compatWith, - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }), - ], - }, - 'ember-cli-babel': { - throwUnlessParallelizable: true, - enableTypeScriptTransform: true, - }, - 'ember-cli-terser': { - exclude: ['assets/dummy.js', 'assets/tests.js', 'assets/test-support.js'], - }, - }); - - /* - This build file specifies the options for the dummy test app of this - addon, located in `/tests/dummy` - This build file does *not* influence how the addon or the app using it - behave. You most likely want to be modifying `./index.js` or app's build file - */ - - return app.toTree(); -}; diff --git a/tests/graph/package.json b/tests/graph/package.json deleted file mode 100644 index 1162d785b13..00000000000 --- a/tests/graph/package.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "name": "graph-test-app", - "version": "4.12.8", - "private": true, - "description": "Provides tests for @ember-data/graph", - "keywords": [], - "repository": { - "type": "git", - "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "tests/graph" - }, - "license": "MIT", - "author": "", - "directories": { - "test": "tests" - }, - "scripts": { - "build": "ember build", - "start": "ember test --test-port=0 --serve --no-launch", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/model": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/private-build-infra": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-blueprint-test-helpers": "^0.19.2", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-sri": "^2.1.1", - "ember-cli-terser": "~4.0.2", - "ember-cli-test-loader": "^3.0.0", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "ember-source-channel-url": "^3.0.0", - "ember-try": "^2.0.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-console-grouper": "^0.3.0", - "qunit-dom": "^2.0.0", - "silent-error": "^1.1.1", - "webpack": "^5.77.0" - }, - "ember": { - "edition": "octane" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/graph/testem.js b/tests/graph/testem.js deleted file mode 100644 index a85540f243d..00000000000 --- a/tests/graph/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - Firefox: { - ci: ['--headless', '--width=1440', '--height=900'], - }, - }, -}; diff --git a/tests/graph/tests/.gitkeep b/tests/graph/tests/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/graph/tests/index.html b/tests/graph/tests/index.html deleted file mode 100644 index c1d72af4d3c..00000000000 --- a/tests/graph/tests/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - Graph Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/tests/graph/tests/integration/graph.ts b/tests/graph/tests/integration/graph.ts deleted file mode 100644 index aa62212bcb1..00000000000 --- a/tests/graph/tests/integration/graph.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { module, test } from 'qunit'; - -module('RecordData', function () { - test('Test Suit Configured', async function (assert) { - assert.ok('We are configured'); - }); -}); diff --git a/tests/graph/tests/integration/graph/edge-removal/setup.ts b/tests/graph/tests/integration/graph/edge-removal/setup.ts deleted file mode 100644 index c2cb5d44aa4..00000000000 --- a/tests/graph/tests/integration/graph/edge-removal/setup.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { setupTest } from 'ember-qunit'; - -import { graphFor } from '@ember-data/graph/-private'; -import type { ImplicitRelationship } from '@ember-data/graph/-private/graph'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import type Store from '@ember-data/store'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import type { - CollectionResourceDocument, - EmptyResourceDocument, - JsonApiDocument, - SingleResourceDocument, -} from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { Dict } from '@ember-data/types/q/utils'; - -class AbstractMap { - constructor(private store: Store, private isImplicit: boolean) {} - - has(identifier: StableRecordIdentifier) { - let graph = graphFor(this.store); - return graph.identifiers.has(identifier); - } -} - -class AbstractGraph { - public identifiers: AbstractMap; - public implicit: { has(identifier: StableRecordIdentifier): boolean }; - - constructor(private store: Store) { - this.identifiers = new AbstractMap(store, false); - this.implicit = { - has: (identifier) => { - return Object.keys(this.getImplicit(identifier)).length > 0; - }, - }; - } - - get( - identifier: StableRecordIdentifier, - propertyName: string - ): ManyRelationship | BelongsToRelationship | ImplicitRelationship { - return graphFor(this.store).get(identifier, propertyName); - } - - getImplicit(identifier: StableRecordIdentifier): Dict { - const rels = graphFor(this.store).identifiers.get(identifier); - let implicits = Object.create(null); - if (rels) { - Object.keys(rels).forEach((key) => { - let rel = rels[key]!; - if (rel && isImplicit(rel)) { - implicits[key] = rel; - } - }); - } - return implicits; - } -} - -function graphForTest(store: Store) { - return new AbstractGraph(store); -} - -export function isBelongsTo( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is BelongsToRelationship { - return relationship.definition.kind === 'belongsTo'; -} - -export function isImplicit( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is ImplicitRelationship { - return relationship.definition.isImplicit; -} - -export function isHasMany( - relationship: ManyRelationship | ImplicitRelationship | BelongsToRelationship -): relationship is ManyRelationship { - return relationship.definition.kind === 'hasMany'; -} - -function setToArray(set: Set): T[] { - return Array.from(set); -} - -export function stateOf(rel: BelongsToRelationship | ManyRelationship | ImplicitRelationship): { - remote: StableRecordIdentifier[]; - local: StableRecordIdentifier[]; -} { - let local: StableRecordIdentifier[]; - let remote: StableRecordIdentifier[]; - - if (isBelongsTo(rel)) { - // we cast these to array form to make the tests more legible - local = rel.localState ? [rel.localState] : []; - remote = rel.remoteState ? [rel.remoteState] : []; - } else if (isHasMany(rel)) { - local = rel.localState.filter((m) => m !== null) as StableRecordIdentifier[]; - remote = rel.remoteState.filter((m) => m !== null) as StableRecordIdentifier[]; - } else { - local = setToArray(rel.localMembers); - remote = setToArray(rel.remoteMembers); - } - return { - local, - remote, - }; -} - -class Adapter { - static create() { - return new this(); - } - static updateRecord() { - return Promise.resolve(); - } - async deleteRecord() { - return { data: null }; - } -} -class Serializer { - static create() { - return new this(); - } - normalizeResponse(_, __, data) { - return data; - } -} - -export interface UserRecord extends DSModel { - name?: string; - bestFriend?: UserRecord; - bestFriends?: UserRecord[]; -} - -export interface Context { - store: TestStore; - graph: AbstractGraph; - owner: any; -} - -interface TestStore extends Store { - push(data: EmptyResourceDocument): null; - push(data: SingleResourceDocument): T; - push(data: CollectionResourceDocument): T[]; - push(data: JsonApiDocument): T | T[] | null; -} - -export function setupGraphTest(hooks) { - setupTest(hooks); - hooks.beforeEach(function (this: Context) { - this.owner.register('adapter:application', Adapter); - this.owner.register('serializer:application', Serializer); - this.store = this.owner.lookup('service:store'); - this.graph = graphForTest(this.store); - }); -} diff --git a/tests/graph/tests/integration/graph/graph-test.ts b/tests/graph/tests/integration/graph/graph-test.ts deleted file mode 100644 index 2ce8ae7ef7b..00000000000 --- a/tests/graph/tests/integration/graph/graph-test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import { graphFor } from '@ember-data/graph/-private'; -import Store from '@ember-data/store'; - -module('Integration | Graph | Configuration', function (hooks) { - setupTest(hooks); - - class MyStore extends Store { - isGraphStore = true; - } - - let store; - hooks.beforeEach(function (assert) { - const { owner } = this; - owner.register('service:store', MyStore); - store = owner.lookup('service:store'); - assert.strictEqual(store.isGraphStore, true, 'pre-cond, store registered correctly'); - }); - - test('graphFor util returns the same graph instance for repeated calls on the same store wrapper instance', async function (assert) { - const wrapper = store._instanceCache._storeWrapper; - const graph1 = graphFor(wrapper); - const graph2 = graphFor(wrapper); - const graph3 = graphFor(wrapper); - - assert.strictEqual(graph1, graph2, 'We got the same instance the second time'); - assert.strictEqual(graph2, graph3, 'We got the same instance the third time'); - }); - - test('graphFor util returns a new graph instance for each unique store wrapper', async function (assert) { - const { owner } = this; - const wrapper1 = store._instanceCache._storeWrapper; - - owner.register('service:store2', MyStore); - owner.register('service:store3', MyStore); - - const store2 = owner.lookup('service:store2') as Store; - const store3 = owner.lookup('service:store3') as Store; - const wrapper2 = store2._instanceCache._storeWrapper; - const wrapper3 = store3._instanceCache._storeWrapper; - - const graph1 = graphFor(wrapper1); - const graph2 = graphFor(wrapper2); - const graph3 = graphFor(wrapper3); - - assert.notStrictEqual(graph1, graph2, 'We got a new instance for store2'); - assert.notStrictEqual(graph1, graph3, 'We got a new instance for store3'); - assert.notStrictEqual(graph2, graph3, 'The instance for store2 is not the same as store3'); - }); - - test('graphFor util returns the same graph instance for repeated calls on the same store instance', async function (assert) { - const graph1 = graphFor(store); - const graph2 = graphFor(store); - const graph3 = graphFor(store); - - assert.strictEqual(graph1, graph2, 'We got the same instance the second time'); - assert.strictEqual(graph2, graph3, 'We got the same instance the third time'); - }); - - test('graphFor util returns a new graph instance for each unique store', async function (assert) { - const { owner } = this; - owner.register('service:store2', MyStore); - owner.register('service:store3', MyStore); - - const store2 = owner.lookup('service:store2') as Store; - const store3 = owner.lookup('service:store3') as Store; - - const graph1 = graphFor(store); - const graph2 = graphFor(store2); - const graph3 = graphFor(store3); - - assert.notStrictEqual(graph1, graph2, 'We got a new instance for store2'); - assert.notStrictEqual(graph1, graph3, 'We got a new instance for store3'); - assert.notStrictEqual(graph2, graph3, 'The instance for store2 is not the same as store3'); - }); - - test('graphFor util returns the same graph instance for the store and storeWrapper', async function (assert) { - const { owner } = this; - const wrapper = store._instanceCache._storeWrapper; - // lookup the wrapper first - const graph1 = graphFor(wrapper); - const graph2 = graphFor(store); - - owner.register('service:store2', MyStore); - const store2 = owner.lookup('service:store2') as Store; - const wrapper2 = store2._instanceCache._storeWrapper; - // lookup the store first - const graph3 = graphFor(store2); - const graph4 = graphFor(wrapper2); - - assert.strictEqual(graph1, graph2, 'We got the same instance when wrapper is looked up first'); - assert.strictEqual(graph3, graph4, 'We got the same instance when store is looked up first'); - assert.notStrictEqual(graph1, graph3, 'The stores do not share an instance'); - }); -}); diff --git a/tests/graph/tests/integration/graph/operations-test.ts b/tests/graph/tests/integration/graph/operations-test.ts deleted file mode 100644 index 4fb7c4ca4d6..00000000000 --- a/tests/graph/tests/integration/graph/operations-test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import { graphFor } from '@ember-data/graph/-private'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; -import Model, { attr, hasMany } from '@ember-data/model'; -import type Store from '@ember-data/store'; - -module('Integration | Graph | Operations', function (hooks: NestedHooks) { - setupTest(hooks); - - test('updateRelationship operation filters duplicates', function (assert: Assert) { - const { owner } = this; - - class App extends Model { - @attr declare name: string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; - } - - class Config extends Model { - @attr declare name: string; - } - - owner.register('model:app', App); - owner.register('model:config', Config); - const store = owner.lookup('service:store') as Store; - const graph = graphFor(store); - const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); - - store._join(() => { - graph.push({ - op: 'updateRelationship', - field: 'configs', - record: appIdentifier, - value: { - data: [ - { type: 'config', id: '1' }, - { type: 'config', id: '1' }, - { type: 'config', id: '1' }, - { type: 'config', id: '2' }, - { type: 'config', id: '3' }, - { type: 'config', id: '4' }, - ], - }, - }); - }); - - const data = graph.get(appIdentifier, 'configs') as ManyRelationship; - assert.deepEqual( - JSON.parse(JSON.stringify(data.getData())), - { - data: [ - { type: 'config', id: '1', lid: '@lid:config-1' }, - { type: 'config', id: '2', lid: '@lid:config-2' }, - { type: 'config', id: '3', lid: '@lid:config-3' }, - { type: 'config', id: '4', lid: '@lid:config-4' }, - ], - }, - 'we have the expected data' - ); - }); - - test('replaceRelatedRecords operation filters duplicates in a local replace', function (assert) { - const { owner } = this; - - class App extends Model { - @attr declare name: string; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - @hasMany('config', { async: false, inverse: null }) declare configs: Config[]; - } - - class Config extends Model { - @attr declare name: string; - } - - owner.register('model:app', App); - owner.register('model:config', Config); - const store = owner.lookup('service:store') as Store; - const graph = graphFor(store); - const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); - const configIdentifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); - const configIdentifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '2' }); - const configIdentifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '3' }); - const configIdentifier4 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); - - store._join(() => { - graph.update({ - op: 'replaceRelatedRecords', - field: 'configs', - record: appIdentifier, - value: [ - configIdentifier1, - configIdentifier1, - configIdentifier1, - configIdentifier2, - configIdentifier3, - configIdentifier4, - ], - }); - }); - - const data = graph.get(appIdentifier, 'configs') as ManyRelationship; - assert.deepEqual( - JSON.parse(JSON.stringify(data.getData())), - { - data: [ - { type: 'config', id: '1', lid: '@lid:config-1' }, - { type: 'config', id: '2', lid: '@lid:config-2' }, - { type: 'config', id: '3', lid: '@lid:config-3' }, - { type: 'config', id: '4', lid: '@lid:config-4' }, - ], - }, - 'we have the expected data' - ); - }); -}); diff --git a/tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts b/tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts deleted file mode 100644 index b186f7a6dac..00000000000 --- a/tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import { graphFor } from '@ember-data/graph/-private'; -import Model, { attr, belongsTo } from '@ember-data/model'; -import Store, { recordIdentifierFor } from '@ember-data/store'; - -module('Integration | Graph | Implicit Keys', function (hooks) { - setupTest(hooks); - - test('Non-polymorphic records do not trigger polymorphic assertions when they share the same key with another record', async function (assert) { - const { owner } = this; - class User extends Model { - @attr name; - @belongsTo('organization', { async: false, inverse: null }) organization; - } - class Product extends Model { - @attr name; - @belongsTo('organization', { async: false, inverse: null }) organization; - } - class Organization extends Model { - @attr name; - } - owner.register('model:user', User); - owner.register('model:product', Product); - owner.register('model:organization', Organization); - - const store = owner.lookup('service:store') as Store; - const graph = graphFor(store); - let user, product, organization; - - assert.expectNoAssertion(() => { - [user, product, organization] = store.push({ - data: [ - { - type: 'user', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - organization: { data: { type: 'organization', id: '1 ' } }, - }, - }, - { - type: 'product', - id: '1', - attributes: { name: 'Awesome Relationships' }, - relationships: { - organization: { data: { type: 'organization', id: '1 ' } }, - }, - }, - { - type: 'organization', - id: '1', - attributes: { name: 'Ember.js' }, - }, - ], - }); - }); - - const userIdentifier = recordIdentifierFor(user); - const productIdentifier = recordIdentifierFor(product); - const organizationIdentifier = recordIdentifierFor(organization); - - const userOrg = graph.get(userIdentifier, 'organization'); - const userImpl = graph.get(organizationIdentifier, userOrg.definition.inverseKey); - const productOrg = graph.get(productIdentifier, 'organization'); - const productImpl = graph.get(organizationIdentifier, productOrg.definition.inverseKey); - - assert.notStrictEqual(userImpl, productImpl, 'We have separate implicit caches'); - }); -}); diff --git a/tests/graph/tests/test-helper.js b/tests/graph/tests/test-helper.js deleted file mode 100644 index 147cf2a3d4d..00000000000 --- a/tests/graph/tests/test-helper.js +++ /dev/null @@ -1,54 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; -import RSVP from 'rsvp'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; -import customQUnitAdapter from '@ember-data/unpublished-test-infra/test-support/testem/custom-qunit-adapter'; - -import Application from '../app'; -import config from '../config/environment'; - -if (window.Promise === undefined) { - window.Promise = RSVP.Promise; -} - -// Handle testing feature flags -if (QUnit.urlParams.enableoptionalfeatures) { - window.EmberDataENV = { ENABLE_OPTIONAL_FEATURES: true }; -} - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -if (window.Testem) { - window.Testem.useCustomAdapter(customQUnitAdapter); -} - -QUnit.begin(function () { - RSVP.configure('onerror', (reason) => { - // only print error messages if they're exceptions; - // otherwise, let a future turn of the event loop - // handle the error. - // TODO kill this off - if (reason && reason instanceof Error) { - throw reason; - } - }); -}); - -QUnit.config.testTimeout = 2000; -QUnit.config.urlConfig.push({ - id: 'enableoptionalfeatures', - label: 'Enable Opt Features', -}); -start({ setupTestIsolationValidation: true }); diff --git a/tests/json-api-encapsulation/.ember-cli b/tests/json-api-encapsulation/.ember-cli deleted file mode 100644 index ee64cfed2a8..00000000000 --- a/tests/json-api-encapsulation/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/tests/json-api-encapsulation/.gitignore b/tests/json-api-encapsulation/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/json-api-encapsulation/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/json-api-encapsulation/.template-lintrc.js b/tests/json-api-encapsulation/.template-lintrc.js deleted file mode 100644 index f35f61c7b3a..00000000000 --- a/tests/json-api-encapsulation/.template-lintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - extends: 'recommended', -}; diff --git a/tests/json-api-encapsulation/.watchmanconfig b/tests/json-api-encapsulation/.watchmanconfig deleted file mode 100644 index e7834e3e4f3..00000000000 --- a/tests/json-api-encapsulation/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/tests/json-api-encapsulation/README.md b/tests/json-api-encapsulation/README.md deleted file mode 100644 index 450c917cc8a..00000000000 --- a/tests/json-api-encapsulation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# encapsulation-test-app - -This README outlines the details of collaborating on this Ember application. -A short introduction of this app could easily go here. - -## Prerequisites - -You will need the following things properly installed on your computer. - -* [Git](https://git-scm.com/) -* [Node.js](https://nodejs.org/) (with npm) -* [Ember CLI](https://ember-cli.com/) -* [Google Chrome](https://google.com/chrome/) - -## Installation - -* `git clone ` this repository -* `cd encapsulation-test-app` -* `npm install` - -## Running / Development - -* `ember serve` -* Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). - -### Code Generators - -Make use of the many generators for code, try `ember help generate` for more details - -### Running Tests - -* `ember test` -* `ember test --server` - -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - -### Building - -* `ember build` (development) -* `ember build --environment production` (production) - -### Deploying - -Specify what it takes to deploy your app. - -## Further Reading / Useful Links - -* [ember.js](https://emberjs.com/) -* [ember-cli](https://ember-cli.com/) -* Development Browser Extensions - * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) - * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/json-api-encapsulation/app/components/.gitkeep b/tests/json-api-encapsulation/app/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/app/controllers/.gitkeep b/tests/json-api-encapsulation/app/controllers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/app/helpers/.gitkeep b/tests/json-api-encapsulation/app/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/app/index.html b/tests/json-api-encapsulation/app/index.html deleted file mode 100644 index 8cb78ffc0a8..00000000000 --- a/tests/json-api-encapsulation/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EncapsulationTestApp - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/json-api-encapsulation/app/models/.gitkeep b/tests/json-api-encapsulation/app/models/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/app/routes/.gitkeep b/tests/json-api-encapsulation/app/routes/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/app/templates/application.hbs b/tests/json-api-encapsulation/app/templates/application.hbs deleted file mode 100644 index ebe6a496046..00000000000 --- a/tests/json-api-encapsulation/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
- {{outlet}} -
\ No newline at end of file diff --git a/tests/json-api-encapsulation/app/templates/components/.gitkeep b/tests/json-api-encapsulation/app/templates/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/config/environment.js b/tests/json-api-encapsulation/config/environment.js deleted file mode 100644 index 4eb76455ed1..00000000000 --- a/tests/json-api-encapsulation/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'json-api-encapsulation-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/json-api-encapsulation/ember-cli-build.js b/tests/json-api-encapsulation/ember-cli-build.js deleted file mode 100644 index 8f86cbd8ec6..00000000000 --- a/tests/json-api-encapsulation/ember-cli-build.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/tests/json-api-encapsulation/package.json b/tests/json-api-encapsulation/package.json deleted file mode 100644 index cafb2338a26..00000000000 --- a/tests/json-api-encapsulation/package.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "name": "json-api-encapsulation-test-app", - "version": "4.12.8", - "private": true, - "description": "Small description for encapsulation-test-app goes here", - "repository": { - "type": "git", - "url": "https://github.com/emberjs/data.git", - "directory": "tests/json-api-encapsulation" - }, - "license": "MIT", - "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", - "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/debug": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/debug": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "webpack": "^5.77.0" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "ember": { - "edition": "octane" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/json-api-encapsulation/public/robots.txt b/tests/json-api-encapsulation/public/robots.txt deleted file mode 100644 index f5916452e5f..00000000000 --- a/tests/json-api-encapsulation/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# http://www.robotstxt.org -User-agent: * -Disallow: diff --git a/tests/json-api-encapsulation/testem.js b/tests/json-api-encapsulation/testem.js deleted file mode 100644 index e10b064501a..00000000000 --- a/tests/json-api-encapsulation/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - }, - Firefox: { - ci: ['-headless', '-width 1440', '-height 900'], - }, -}; diff --git a/tests/json-api-encapsulation/tests/helpers/.gitkeep b/tests/json-api-encapsulation/tests/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/tests/index.html b/tests/json-api-encapsulation/tests/index.html deleted file mode 100644 index 8c6b27bc403..00000000000 --- a/tests/json-api-encapsulation/tests/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - EncapsulationTestApp Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/tests/json-api-encapsulation/tests/integration/.gitkeep b/tests/json-api-encapsulation/tests/integration/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/tests/integration/smoke-test.js b/tests/json-api-encapsulation/tests/integration/smoke-test.js deleted file mode 100644 index a30eae15b9d..00000000000 --- a/tests/json-api-encapsulation/tests/integration/smoke-test.js +++ /dev/null @@ -1,50 +0,0 @@ -/* global require */ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Store from '@ember-data/store'; - -function assertPackageNotPresent(packageName, assert) { - const entries = Object.keys(require.entries); - const entriesFromPackage = entries.filter((m) => m.indexOf(packageName) === 0); - const importedDependencies = {}; - const entriesImportingPackage = entries.filter((m) => { - const deps = require.entries[m].deps; - const moduleDeps = deps.filter((d) => d.indexOf(packageName) === 0); - - if (moduleDeps.length) { - importedDependencies[m] = moduleDeps; - } - return moduleDeps.length > 0; - }); - - assert.ok(entries.length > 0, 'We have modules'); - assert.ok( - entriesFromPackage.length === 0, - `We expect no modules from ${packageName} ${ - entriesFromPackage.length > 0 ? `found: [\n\t"${entriesFromPackage.join('",\n\t"')}"\n]` : '' - }` - ); - assert.ok( - entriesImportingPackage.length === 0, - `We expect no modules with dependencies on ${packageName} ${ - entriesImportingPackage.length > 0 ? `found:\n${JSON.stringify(importedDependencies, null, 2)}` : '' - }` - ); -} - -module('Record-data Encapsulation - Smoke Tests', function (hooks) { - setupTest(hooks); - hooks.beforeEach(function () { - this.owner.register('service:store', Store); - }); - - test('No @ember-data/json-api modules are present', function (assert) { - assertPackageNotPresent('@ember-data/json-api', assert); - }); - - test('No ember-data modules are present', function (assert) { - assertPackageNotPresent('ember-data', assert); - }); -}); diff --git a/tests/json-api-encapsulation/tests/test-helper.js b/tests/json-api-encapsulation/tests/test-helper.js deleted file mode 100644 index a16f69329b5..00000000000 --- a/tests/json-api-encapsulation/tests/test-helper.js +++ /dev/null @@ -1,23 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/tests/json-api-encapsulation/tests/unit/.gitkeep b/tests/json-api-encapsulation/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api-encapsulation/vendor/.gitkeep b/tests/json-api-encapsulation/vendor/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api/README.md b/tests/json-api/README.md deleted file mode 100644 index 1280a75cee1..00000000000 --- a/tests/json-api/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# json-api-tests - -Provides testing for the JSON:API Cache Implementation diff --git a/tests/json-api/app/app.ts b/tests/json-api/app/app.ts deleted file mode 100644 index 1f39476ae86..00000000000 --- a/tests/json-api/app/app.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -class App extends Application { - modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; - Resolver = Resolver; -} - -loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/tests/json-api/app/index.html b/tests/json-api/app/index.html deleted file mode 100644 index 9ea0b889ef1..00000000000 --- a/tests/json-api/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EmberData Graph Test App - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/json-api/app/templates/.gitkeep b/tests/json-api/app/templates/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api/app/templates/application.hbs b/tests/json-api/app/templates/application.hbs deleted file mode 100644 index 70772af7e9d..00000000000 --- a/tests/json-api/app/templates/application.hbs +++ /dev/null @@ -1,7 +0,0 @@ -
-

EmberData JSON:API Tests

- - {{outlet}} - - Tests -
diff --git a/tests/json-api/config/environment.js b/tests/json-api/config/environment.js deleted file mode 100644 index 869bb3bd6b9..00000000000 --- a/tests/json-api/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'json-api-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/json-api/ember-cli-build.js b/tests/json-api/ember-cli-build.js deleted file mode 100644 index a414b06d059..00000000000 --- a/tests/json-api/ember-cli-build.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - const compatWith = process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : '4.12'; - let app = new EmberApp(defaults, { - emberData: { - compatWith, - }, - babel: { - // this ensures that the same build-time code stripping that is done - // for library packages is also done for our tests and dummy app - plugins: [ - ...require('@ember-data/private-build-infra/src/debug-macros')({ - compatWith, - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }), - ], - }, - 'ember-cli-babel': { - throwUnlessParallelizable: true, - enableTypeScriptTransform: true, - }, - 'ember-cli-terser': { - exclude: ['assets/dummy.js', 'assets/tests.js', 'assets/test-support.js'], - }, - }); - - /* - This build file specifies the options for the dummy test app of this - addon, located in `/tests/dummy` - This build file does *not* influence how the addon or the app using it - behave. You most likely want to be modifying `./index.js` or app's build file - */ - - return app.toTree(); -}; diff --git a/tests/json-api/package.json b/tests/json-api/package.json deleted file mode 100644 index 7ea494c28c5..00000000000 --- a/tests/json-api/package.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "name": "json-api-test-app", - "version": "4.12.8", - "private": true, - "description": "Provides tests for @ember-data/json-api", - "keywords": [], - "repository": { - "type": "git", - "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "tests/graph" - }, - "license": "MIT", - "author": "", - "directories": { - "test": "tests" - }, - "scripts": { - "build": "ember build", - "start": "ember test --test-port=0 --serve --no-launch", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/private-build-infra": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-blueprint-test-helpers": "^0.19.2", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-sri": "^2.1.1", - "ember-cli-terser": "~4.0.2", - "ember-cli-test-loader": "^3.0.0", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "ember-source-channel-url": "^3.0.0", - "ember-try": "^2.0.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-console-grouper": "^0.3.0", - "qunit-dom": "^2.0.0", - "silent-error": "^1.1.1", - "webpack": "^5.77.0" - }, - "ember": { - "edition": "octane" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/json-api/testem.js b/tests/json-api/testem.js deleted file mode 100644 index a85540f243d..00000000000 --- a/tests/json-api/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - Firefox: { - ci: ['--headless', '--width=1440', '--height=900'], - }, - }, -}; diff --git a/tests/json-api/tests/.gitkeep b/tests/json-api/tests/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/json-api/tests/index.html b/tests/json-api/tests/index.html deleted file mode 100644 index a4dd74b68ad..00000000000 --- a/tests/json-api/tests/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - JSON:API Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
-
-
-
-
-
- - - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/tests/json-api/tests/integration/-smoke.ts b/tests/json-api/tests/integration/-smoke.ts deleted file mode 100644 index 171b4886168..00000000000 --- a/tests/json-api/tests/integration/-smoke.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { module, test } from 'qunit'; - -module('Cache', function () { - test('Test Suit Configured', function (assert) { - assert.ok('We are configured'); - }); -}); diff --git a/tests/json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/json-api/tests/integration/cache/collection-data-documents-test.ts deleted file mode 100644 index 04fa0e2b687..00000000000 --- a/tests/json-api/tests/integration/cache/collection-data-documents-test.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Cache from '@ember-data/json-api'; -import Store from '@ember-data/store'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type { CollectionResourceDataDocument, StructuredDocument } from '@ember-data/types/cache/document'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import { DSModel } from '@ember-data/types/q/ds-model'; -import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; - -type FakeRecord = { [key: string]: unknown; destroy: () => void }; -class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } - - instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); - - let token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - teardownRecord(record: FakeRecord) { - record.destroy(); - } -} - -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas: Schemas) { - this.schemas = schemas; - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type === 'user'; - } -} - -module('Integration | @ember-data/json-api Cache.put()', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', TestStore); - }); - - test('simple collection resource documents are correctly managed', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - content: { - data: [ - { type: 'user', id: '1', attributes: { name: 'Chris' } }, - { type: 'user', id: '2', attributes: { name: 'Wesley' } }, - ], - }, - } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - - assert.deepEqual(responseDocument.data, [identifier, identifier2], 'We were given the correct data back'); - }); - - test('collection resource documents are correctly cached', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - data: [ - { type: 'user', id: '1', attributes: { name: 'Chris' } }, - { type: 'user', id: '2', attributes: { name: 'Wesley' } }, - ], - }, - } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - - assert.deepEqual(responseDocument.data, [identifier, identifier2], 'We were given the correct data back'); - - const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - data: [identifier, identifier2], - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse, - { - lid: 'https://api.example.com/v1/users', - data: [identifier, identifier2], - }, - 'We got the cached response document back' - ); - }); - - test('resources are accessible via `peek`', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - content: { - data: [{ type: 'user', id: '1', attributes: { name: 'Chris' } }], - }, - } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.deepEqual(responseDocument.data, [identifier], 'We were given the correct data back'); - - let resourceData = store.cache.peek(identifier); - - assert.deepEqual( - resourceData, - { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, - 'We can fetch from the cache' - ); - - const record = store.peekRecord(identifier) as DSModel; - - assert.strictEqual(record.name, 'Chris', 'record name is correct'); - - store.cache.setAttr(identifier, 'name', 'James'); - resourceData = store.cache.peek(identifier); - - assert.deepEqual( - resourceData, - { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'James' }, relationships: {} }, - 'Resource Blob is kept updated in the cache after mutation' - ); - - store.cache.put({ - content: { - data: [{ type: 'user', id: '1', attributes: { username: '@runspired' } }], - }, - } as StructuredDocument); - - resourceData = store.cache.peek(identifier); - assert.deepEqual( - resourceData, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'James', username: '@runspired' }, - relationships: {}, - }, - 'Resource Blob is kept updated in the cache after additional put' - ); - - store.cache.rollbackAttrs(identifier); - resourceData = store.cache.peek(identifier); - assert.deepEqual( - resourceData, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'Chris', username: '@runspired' }, - relationships: {}, - }, - 'Resource Blob is kept updated in the cache after rollback' - ); - }); - - test('resource relationships are accessible via `peek`', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - store.registerSchemaDefinitionService( - new TestSchema<'user'>({ - user: { - attributes: { - name: { kind: 'attribute', name: 'name' }, - }, - relationships: { - bestFriend: { - kind: 'belongsTo', - type: 'user', - key: 'bestFriend', - name: 'bestFriend', - options: { - async: false, - inverse: 'bestFriend', - }, - }, - worstEnemy: { - kind: 'belongsTo', - type: 'user', - key: 'worstEnemy', - name: 'worstEnemy', - options: { - async: false, - inverse: null, - }, - }, - friends: { - kind: 'hasMany', - type: 'user', - key: 'friends', - name: 'friends', - options: { - async: false, - inverse: 'friends', - }, - }, - }, - }, - }) - ); - - let responseDocument: CollectionResourceDataDocument; - store._run(() => { - responseDocument = store.cache.put({ - content: { - data: [ - { - type: 'user', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - bestFriend: { - data: { type: 'user', id: '2' }, - }, - worstEnemy: { - data: { type: 'user', id: '3' }, - }, - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - ], - }, - }, - }, - ], - included: [ - { - type: 'user', - id: '2', - attributes: { name: 'Wesley' }, - relationships: { - bestFriend: { - data: { type: 'user', id: '1' }, - }, - friends: { - data: [ - { type: 'user', id: '1' }, - { type: 'user', id: '3' }, - ], - }, - }, - }, - { - type: 'user', - id: '3', - attributes: { name: 'Rey' }, - relationships: { - bestFriend: { - data: null, - }, - friends: { - data: [ - { type: 'user', id: '1' }, - { type: 'user', id: '2' }, - ], - }, - }, - }, - ], - }, - } as StructuredDocument) as CollectionResourceDataDocument; - }); - const identifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const identifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); - - assert.deepEqual(responseDocument!.data, [identifier1], 'We were given the correct data back'); - - let resourceData1 = store.cache.peek(identifier1); - let resourceData2 = store.cache.peek(identifier2); - let resourceData3 = store.cache.peek(identifier3); - - assert.deepEqual( - resourceData1, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'Chris' }, - relationships: { - bestFriend: { - data: identifier2, - }, - friends: { - data: [identifier2, identifier3], - }, - worstEnemy: { - data: identifier3, - }, - }, - }, - 'We can fetch from the cache' - ); - assert.deepEqual( - resourceData2, - { - type: 'user', - id: '2', - lid: '@lid:user-2', - attributes: { name: 'Wesley' }, - relationships: { - bestFriend: { - data: identifier1, - }, - friends: { - data: [identifier1, identifier3], - }, - }, - }, - 'We can fetch included data from the cache' - ); - assert.deepEqual( - resourceData3, - { - type: 'user', - id: '3', - lid: '@lid:user-3', - attributes: { name: 'Rey' }, - relationships: { - bestFriend: { - data: null, - }, - friends: { - data: [identifier1, identifier2], - }, - }, - }, - 'We can fetch more included data from the cache' - ); - }); -}); diff --git a/tests/json-api/tests/integration/cache/meta-documents-test.ts b/tests/json-api/tests/integration/cache/meta-documents-test.ts deleted file mode 100644 index 929d548ecff..00000000000 --- a/tests/json-api/tests/integration/cache/meta-documents-test.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Cache from '@ember-data/json-api'; -import Store from '@ember-data/store'; -import { CacheOperation } from '@ember-data/store/-private/managers/notification-manager'; -import type { - CollectionResourceDataDocument, - ResourceMetaDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; - -class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } -} - -module('Integration | @ember-data/json-api Cach.put()', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', TestStore); - }); - - test('meta documents are correctly cached', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - meta: { count: 4 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.false('data' in responseDocument, 'No data is associated'); - assert.deepEqual(responseDocument.meta, { count: 4 }, 'meta is correct'); - assert.strictEqual(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4 }), 'meta is correct'); - assert.strictEqual(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); - - const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - meta: { count: 4 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse, - { - lid: 'https://api.example.com/v1/users', - meta: { count: 4 }, - }, - 'We got the cached response document back' - ); - }); - - test('meta documents respect cacheOptions.key', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users', cacheOptions: { key: 'users' } }, - content: { - meta: { count: 4 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.false('data' in responseDocument, 'No data is associated'); - assert.deepEqual(responseDocument.meta, { count: 4 }, 'meta is correct'); - assert.strictEqual(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4 }), 'meta is correct'); - assert.strictEqual(responseDocument.lid, 'users', 'lid is correct'); - - const structuredDocument = store.cache.peekRequest({ lid: 'users' }); - const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.strictEqual(structuredDocument2, null, 'url is not cache key'); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users', cacheOptions: { key: 'users' } }, - content: { - lid: 'users', - meta: { count: 4 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'users' }); - const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.strictEqual(cachedResponse2, null, 'url is not cache key'); - assert.deepEqual( - cachedResponse, - { - lid: 'users', - meta: { count: 4 }, - }, - 'We got the cached response document back' - ); - }); - - test('meta documents are correctly updated', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - meta: { count: 4, last: 4 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.false('data' in responseDocument, 'No data is associated'); - assert.deepEqual(responseDocument.meta, { count: 4, last: 4 }, 'meta is correct'); - assert.strictEqual(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4, last: 4 }), 'meta is correct'); - assert.strictEqual(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); - - const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - meta: { count: 4, last: 4 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse, - { - lid: 'https://api.example.com/v1/users', - meta: { count: 4, last: 4 }, - }, - 'We got the cached response document back' - ); - - const responseDocument2 = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - meta: { count: 3, next: 8 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.false('data' in responseDocument2, 'No data is associated'); - assert.deepEqual(responseDocument2.meta, { count: 3, next: 8 }, 'meta is correct'); - assert.strictEqual( - JSON.stringify(responseDocument2.meta), - JSON.stringify({ count: 3, next: 8 }), - 'meta is correct' - ); - assert.strictEqual(responseDocument2.lid, 'https://api.example.com/v1/users', 'lid is correct'); - - const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument2, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - meta: { count: 3, next: 8 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse2, - { - lid: 'https://api.example.com/v1/users', - meta: { count: 3, next: 8 }, - }, - 'We got the cached response document back' - ); - }); - - test('updating cache with a meta document disregards prior data', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - data: [{ type: 'user', id: '1', attributes: { name: 'Chris' } }], - meta: { count: 4, last: 4 }, - }, - } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.deepEqual(responseDocument.data, [identifier], 'data is associated'); - assert.deepEqual(responseDocument.meta, { count: 4, last: 4 }, 'meta is correct'); - assert.strictEqual(JSON.stringify(responseDocument.meta), JSON.stringify({ count: 4, last: 4 }), 'meta is correct'); - assert.strictEqual(responseDocument.lid, 'https://api.example.com/v1/users', 'lid is correct'); - - const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - data: [identifier], - meta: { count: 4, last: 4 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse, - { - lid: 'https://api.example.com/v1/users', - data: [identifier], - meta: { count: 4, last: 4 }, - }, - 'We got the cached response document back' - ); - - const responseDocument2 = store.cache.put({ - request: { url: 'https://api.example.com/v1/users' }, - content: { - meta: { count: 3, next: 8 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.false('data' in responseDocument2, 'No data is associated'); - assert.deepEqual(responseDocument2.meta, { count: 3, next: 8 }, 'meta is correct'); - assert.strictEqual( - JSON.stringify(responseDocument2.meta), - JSON.stringify({ count: 3, next: 8 }), - 'meta is correct' - ); - assert.strictEqual(responseDocument2.lid, 'https://api.example.com/v1/users', 'lid is correct'); - - const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - structuredDocument2, - { - request: { url: 'https://api.example.com/v1/users' }, - content: { - lid: 'https://api.example.com/v1/users', - meta: { count: 3, next: 8 }, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users' }); - assert.deepEqual( - cachedResponse2, - { - lid: 'https://api.example.com/v1/users', - meta: { count: 3, next: 8 }, - }, - 'We got the cached response document back' - ); - }); - - test("notifications are generated for create and update of the document's cache key", function (assert) { - assert.expect(10); - const store = this.owner.lookup('service:store') as Store; - const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ - url: '/api/v1/query?type=user&name=Chris&limit=1', - })!; - - let isUpdating = false; - store.notifications.subscribe('document', (identifier: StableDocumentIdentifier, type: CacheOperation) => { - if (isUpdating) { - assert.strictEqual(type, 'updated', 'We were notified of an update'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } else { - assert.strictEqual(type, 'added', 'We were notified of an add'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } - }); - - store.notifications.subscribe(documentIdentifier, (identifier: StableDocumentIdentifier, type: CacheOperation) => { - if (isUpdating) { - assert.strictEqual(type, 'updated', 'We were notified of an update'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } else { - assert.strictEqual(type, 'added', 'We were notified of an add'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } - }); - - store._run(() => { - const responseDocument = store.cache.put({ - request: { - url: '/api/v1/query?type=user&name=Chris&limit=1', - }, - content: { - meta: { count: 4 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.strictEqual(responseDocument.meta.count, 4, 'We were given the correct data back'); - }); - - isUpdating = true; - store._run(() => { - const responseDocument2 = store.cache.put({ - request: { - url: '/api/v1/query?type=user&name=Chris&limit=1', - }, - content: { - meta: { count: 3 }, - }, - } as StructuredDocument) as ResourceMetaDocument; - - assert.strictEqual(responseDocument2.meta.count, 3, 'We were given the correct data back'); - }); - }); -}); diff --git a/tests/json-api/tests/integration/cache/resource-data-documents-test.ts b/tests/json-api/tests/integration/cache/resource-data-documents-test.ts deleted file mode 100644 index 566e0ed9547..00000000000 --- a/tests/json-api/tests/integration/cache/resource-data-documents-test.ts +++ /dev/null @@ -1,536 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Cache from '@ember-data/json-api'; -import Store from '@ember-data/store'; -import type { CacheOperation, NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type { SingleResourceDataDocument, StructuredDocument } from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import type { SingleResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; - -type FakeRecord = { [key: string]: unknown; destroy: () => void }; -class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } - - instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); - - let token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - teardownRecord(record: FakeRecord) { - record.destroy(); - } -} - -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas: Schemas) { - this.schemas = schemas; - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type === 'user'; - } -} - -module('Integration | @ember-data/json-api Cache.put()', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - this.owner.register('service:store', TestStore); - }); - - test('simple single resource documents are correctly managed', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - content: { - data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); - }); - - test('single resource documents are correctly cached', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users/1' }, - content: { - data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); - - const structuredDocument = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users/1' }); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users/1' }, - content: { - lid: 'https://api.example.com/v1/users/1', - data: identifier, - }, - }, - 'We got the cached structured document back' - ); - const cachedResponse = store.cache.peek({ lid: 'https://api.example.com/v1/users/1' }); - assert.deepEqual( - cachedResponse, - { - lid: 'https://api.example.com/v1/users/1', - data: identifier, - }, - 'We got the cached response document back' - ); - }); - - test('data documents respect cacheOptions.key', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - request: { url: 'https://api.example.com/v1/users/1', cacheOptions: { key: 'user-1' } }, - content: { - data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); - - const structuredDocument = store.cache.peekRequest({ lid: 'user-1' }); - const structuredDocument2 = store.cache.peekRequest({ lid: 'https://api.example.com/v1/users/1' }); - assert.strictEqual(structuredDocument2, null, 'we did not use the url as the key'); - assert.deepEqual( - structuredDocument, - { - request: { url: 'https://api.example.com/v1/users/1', cacheOptions: { key: 'user-1' } }, - content: { - lid: 'user-1', - data: identifier, - }, - }, - 'We got the cached structured document back' - ); - - const cachedResponse = store.cache.peek({ lid: 'user-1' }); - const cachedResponse2 = store.cache.peek({ lid: 'https://api.example.com/v1/users/1' }); - assert.strictEqual(cachedResponse2, null, 'we did not use the url as the key'); - assert.deepEqual( - cachedResponse, - { - lid: 'user-1', - data: identifier, - }, - 'We got the cached response document back' - ); - }); - - test("notifications are generated for create and update of the document's cache key", function (assert) { - assert.expect(10); - const store = this.owner.lookup('service:store') as Store; - const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ - url: '/api/v1/query?type=user&name=Chris&limit=1', - })!; - - let isUpdating = false; - store.notifications.subscribe('document', (identifier: StableDocumentIdentifier, type: CacheOperation) => { - if (isUpdating) { - assert.strictEqual(type, 'updated', 'We were notified of an update'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } else { - assert.strictEqual(type, 'added', 'We were notified of an add'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } - }); - - store.notifications.subscribe(documentIdentifier, (identifier: StableDocumentIdentifier, type: CacheOperation) => { - if (isUpdating) { - assert.strictEqual(type, 'updated', 'We were notified of an update'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } else { - assert.strictEqual(type, 'added', 'We were notified of an add'); - assert.strictEqual(identifier, documentIdentifier, 'We were notified of the correct document'); - } - }); - - store._run(() => { - const responseDocument = store.cache.put({ - request: { - url: '/api/v1/query?type=user&name=Chris&limit=1', - }, - content: { - data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); - }); - - isUpdating = true; - store._run(() => { - const responseDocument2 = store.cache.put({ - request: { - url: '/api/v1/query?type=user&name=Chris&limit=1', - }, - content: { - data: { type: 'user', id: '2', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - assert.strictEqual(responseDocument2.data, identifier2, 'We were given the correct data back'); - }); - }); - - test('resources are accessible via `peek`', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - const responseDocument = store.cache.put({ - content: { - data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, - }, - } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); - - let resourceData = store.cache.peek(identifier); - - assert.deepEqual( - resourceData, - { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, - 'We can fetch from the cache' - ); - - const record = store.peekRecord(identifier) as DSModel; - - assert.strictEqual(record.name, 'Chris', 'record name is correct'); - - store.cache.setAttr(identifier, 'name', 'James'); - resourceData = store.cache.peek(identifier); - - assert.deepEqual( - resourceData, - { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'James' }, relationships: {} }, - 'Resource Blob is kept updated in the cache after mutation' - ); - - store.cache.put({ - content: { - data: { type: 'user', id: '1', attributes: { username: '@runspired' } }, - }, - } as StructuredDocument); - - resourceData = store.cache.peek(identifier); - assert.deepEqual( - resourceData, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'James', username: '@runspired' }, - relationships: {}, - }, - 'Resource Blob is kept updated in the cache after additional put' - ); - - store.cache.rollbackAttrs(identifier); - resourceData = store.cache.peek(identifier); - assert.deepEqual( - resourceData, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'Chris', username: '@runspired' }, - relationships: {}, - }, - 'Resource Blob is kept updated in the cache after rollback' - ); - }); - - test('single resource relationships are accessible via `peek`', function (assert) { - const store = this.owner.lookup('service:store') as Store; - - store.registerSchemaDefinitionService( - new TestSchema<'user'>({ - user: { - attributes: { - name: { kind: 'attribute', name: 'name' }, - }, - relationships: { - bestFriend: { - kind: 'belongsTo', - type: 'user', - key: 'bestFriend', - name: 'bestFriend', - options: { - async: false, - inverse: 'bestFriend', - }, - }, - worstEnemy: { - kind: 'belongsTo', - type: 'user', - key: 'worstEnemy', - name: 'worstEnemy', - options: { - async: false, - inverse: null, - }, - }, - friends: { - kind: 'hasMany', - type: 'user', - key: 'friends', - name: 'friends', - options: { - async: false, - inverse: 'friends', - }, - }, - }, - }, - }) - ); - - let responseDocument: SingleResourceDataDocument; - store._run(() => { - responseDocument = store.cache.put({ - content: { - data: { - type: 'user', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - bestFriend: { - data: { type: 'user', id: '2' }, - }, - worstEnemy: { - data: { type: 'user', id: '3' }, - }, - friends: { - data: [ - { type: 'user', id: '2' }, - { type: 'user', id: '3' }, - ], - }, - }, - }, - included: [ - { - type: 'user', - id: '2', - attributes: { name: 'Wesley' }, - relationships: { - bestFriend: { - data: { type: 'user', id: '1' }, - }, - friends: { - data: [ - { type: 'user', id: '1' }, - { type: 'user', id: '3' }, - ], - }, - }, - }, - { - type: 'user', - id: '3', - attributes: { name: 'Rey' }, - relationships: { - bestFriend: { - data: null, - }, - friends: { - data: [ - { type: 'user', id: '1' }, - { type: 'user', id: '2' }, - ], - }, - }, - }, - ], - }, - } as StructuredDocument) as SingleResourceDataDocument; - }); - const identifier1 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const identifier3 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '3' }); - - assert.strictEqual(responseDocument!.data, identifier1, 'We were given the correct data back'); - - let resourceData1 = store.cache.peek(identifier1); - let resourceData2 = store.cache.peek(identifier2); - let resourceData3 = store.cache.peek(identifier3); - - assert.deepEqual( - resourceData1, - { - type: 'user', - id: '1', - lid: '@lid:user-1', - attributes: { name: 'Chris' }, - relationships: { - bestFriend: { - data: identifier2, - }, - friends: { - data: [identifier2, identifier3], - }, - worstEnemy: { - data: identifier3, - }, - }, - }, - 'We can fetch from the cache' - ); - assert.deepEqual( - resourceData2, - { - type: 'user', - id: '2', - lid: '@lid:user-2', - attributes: { name: 'Wesley' }, - relationships: { - bestFriend: { - data: identifier1, - }, - friends: { - data: [identifier1, identifier3], - }, - }, - }, - 'We can fetch included data from the cache' - ); - assert.deepEqual( - resourceData3, - { - type: 'user', - id: '3', - lid: '@lid:user-3', - attributes: { name: 'Rey' }, - relationships: { - bestFriend: { - data: null, - }, - friends: { - data: [identifier1, identifier2], - }, - }, - }, - 'We can fetch more included data from the cache' - ); - }); - - test('generated default values are retained', function (assert) { - const store = new TestStore(); - let i = 0; - - store.registerSchema( - new TestSchema<'user'>({ - user: { - attributes: { - name: { - kind: 'attribute', - name: 'name', - type: 'string', - options: { - defaultValue: () => { - i++; - return `Name ${i}`; - }, - }, - }, - }, - relationships: {}, - }, - }) - ); - - store._run(() => { - store.cache.put({ - content: { - data: { - type: 'user', - id: '1', - attributes: {}, - }, - }, - }) as SingleResourceDataDocument; - }); - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - - const name1 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name1, 'Name 1', 'The default value was generated'); - const name2 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name2, 'Name 1', 'The default value was cached'); - - store.cache.setAttr(identifier, 'name', 'Chris'); - const name3 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name3, 'Chris', 'The value was updated'); - - store.cache.setAttr(identifier, 'name', null); - const name4 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name4, null, 'Null was set and maintained'); - - store.cache.rollbackAttrs(identifier); - const name5 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name5, 'Name 2', 'The default value was regenerated'); - - store._run(() => { - store.cache.put({ - content: { - data: { - type: 'user', - id: '1', - attributes: { - name: 'Tomster', - }, - }, - }, - }) as SingleResourceDataDocument; - }); - - const name6 = store.cache.getAttr(identifier, 'name'); - assert.strictEqual(name6, 'Tomster', 'The value was updated on put'); - }); -}); diff --git a/tests/json-api/tests/test-helper.js b/tests/json-api/tests/test-helper.js deleted file mode 100644 index 147cf2a3d4d..00000000000 --- a/tests/json-api/tests/test-helper.js +++ /dev/null @@ -1,54 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; -import RSVP from 'rsvp'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; -import customQUnitAdapter from '@ember-data/unpublished-test-infra/test-support/testem/custom-qunit-adapter'; - -import Application from '../app'; -import config from '../config/environment'; - -if (window.Promise === undefined) { - window.Promise = RSVP.Promise; -} - -// Handle testing feature flags -if (QUnit.urlParams.enableoptionalfeatures) { - window.EmberDataENV = { ENABLE_OPTIONAL_FEATURES: true }; -} - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -if (window.Testem) { - window.Testem.useCustomAdapter(customQUnitAdapter); -} - -QUnit.begin(function () { - RSVP.configure('onerror', (reason) => { - // only print error messages if they're exceptions; - // otherwise, let a future turn of the event loop - // handle the error. - // TODO kill this off - if (reason && reason instanceof Error) { - throw reason; - } - }); -}); - -QUnit.config.testTimeout = 2000; -QUnit.config.urlConfig.push({ - id: 'enableoptionalfeatures', - label: 'Enable Opt Features', -}); -start({ setupTestIsolationValidation: true }); diff --git a/tests/main/app/app.ts b/tests/main/app/app.ts index bc2162ddfd0..fc6f593cae2 100644 --- a/tests/main/app/app.ts +++ b/tests/main/app/app.ts @@ -41,8 +41,8 @@ const EventConfig = { class App extends Application { modulePrefix = config.modulePrefix; podModulePrefix = config.podModulePrefix; - Resolver = Resolver; - customEvents = EventConfig; + override Resolver = Resolver; + override customEvents = EventConfig; } loadInitializers(App, config.modulePrefix); diff --git a/tests/main/app/backburner-flush-override.js b/tests/main/app/backburner-flush-override.js deleted file mode 100644 index b4fdbbe15d2..00000000000 --- a/tests/main/app/backburner-flush-override.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - An alternative render flush mechanism for glimmer -*/ -import { Renderer } from '@ember/-internals/glimmer'; -import { _backburner } from '@ember/runloop'; - -import * as RSVP from 'rsvp'; - -const HAS_RAF = typeof requestAnimationFrame !== 'undefined'; -const HAS_RIC = typeof requestIdleCallback !== 'undefined'; -const RENDER_DEBOUNCE_COUNT = 5; -const MICROTASK_DEBOUNCE_COUNT = 2; - -// race various things to flush -function _performRace(resolve) { - let resolved = false; - const complete = () => { - if (resolved) return; - resolved = true; - resolve(); - }; - - if (HAS_RAF) - requestAnimationFrame(() => { - if (!resolved) { - if (HAS_RIC) requestIdleCallback(complete); - setTimeout(complete, 0); - } - }); - if (HAS_RIC) requestIdleCallback(complete); - setTimeout(complete, 0); -} - -function race() { - return new Promise(_performRace); -} - -async function awaitSettled(renderer, debounceCount = MICROTASK_DEBOUNCE_COUNT) { - let startCount = renderer._revalidateCalls; - let successCount = 0; - let keepWaiting = true; - - // once we've elected to flush, we wait for the current microtask queue - // to "settle" to begin - while (keepWaiting) { - await Promise.resolve(); - - if (renderer._revalidateCalls === startCount) { - successCount++; - - if (successCount === debounceCount) { - // break loop - keepWaiting = false; - break; - } - } else { - startCount = this._revalidateCalls; - successCount = 0; - } - } - - renderer._revalidateCalls = 0; - renderer._revalidate(); - renderer._nextRevalidate = null; -} - -// add this to tests/main to find tests that only work due to rsvp Promise flush -export function restoreRSVP() { - // restore native promise behavior to RSVP - RSVP.configure('async', (callback, promise) => { - Promise.resolve().then(() => { - callback(promise); - }); - }); -} - -// add this to tests/main to find tests that only work due to backburner flush timing -export function installOverride(debounceCount = MICROTASK_DEBOUNCE_COUNT) { - // debounce autoruns to capture more potential activity - const flush = _backburner._boundAutorunEnd; - const original = _backburner._ensureInstance; - - _backburner._revalidateCalls = 0; - _backburner._ensureInstance = function () { - _backburner._scheduleInstanceCounter++; - return original.apply(_backburner, arguments); - }; - _backburner._revalidate = flush; - _backburner._platform.next = () => { - awaitSettled(_backburner, debounceCount); - }; -} - -// add this to tests/main to find tests that only work due to render flush timing -// This doesn't quite work yet, likely need to patch Ember to remove the heavy -// runloop validation entanglements. -export function installRendererOverride(debounceCount = RENDER_DEBOUNCE_COUNT) { - Renderer.prototype._scheduleRevalidate = function betterRenderFlush() { - if (this._revalidateCalls) { - this._revalidateCalls++; - } else { - this._revalidateCalls = 1; - } - if (!this._nextRevalidate) this._nextRevalidate = race().then(() => awaitSettled(this, debounceCount)); - }; -} diff --git a/tests/main/app/models/foo.js b/tests/main/app/models/foo.js deleted file mode 100644 index 774cf4568d4..00000000000 --- a/tests/main/app/models/foo.js +++ /dev/null @@ -1,7 +0,0 @@ -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - -export default class Foo extends Model { - @attr name; - @belongsTo('foo', { async: false, inverse: 'children' }) parent; - @hasMany('foo', { async: false, inverse: 'parent' }) children; -} diff --git a/tests/main/app/models/foo.ts b/tests/main/app/models/foo.ts new file mode 100644 index 00000000000..2d6e830193c --- /dev/null +++ b/tests/main/app/models/foo.ts @@ -0,0 +1,10 @@ +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type { Type } from '@warp-drive/core-types/symbols'; + +export default class Foo extends Model { + @attr declare name: string | null; + @belongsTo('foo', { async: false, inverse: 'children' }) declare parent: Foo; + @hasMany('foo', { async: false, inverse: 'parent' }) declare children: Foo[]; + + declare [Type]: 'foo'; +} diff --git a/tests/main/app/route.ts b/tests/main/app/route.ts index 6d94d5c6cd4..92acf0bce89 100644 --- a/tests/main/app/route.ts +++ b/tests/main/app/route.ts @@ -6,7 +6,7 @@ import type Store from '@ember-data/store'; export default class ApplicationRoute extends Route { @inject declare store: Store; - model() { + override model() { // adding a model to the store to enable manually testing the debug-adapter return this.store.push({ data: { diff --git a/tests/main/app/routes/application.ts b/tests/main/app/routes/application.ts index 6d94d5c6cd4..92acf0bce89 100644 --- a/tests/main/app/routes/application.ts +++ b/tests/main/app/routes/application.ts @@ -6,7 +6,7 @@ import type Store from '@ember-data/store'; export default class ApplicationRoute extends Route { @inject declare store: Store; - model() { + override model() { // adding a model to the store to enable manually testing the debug-adapter return this.store.push({ data: { diff --git a/tests/main/app/services/store.js b/tests/main/app/services/store.js new file mode 100644 index 00000000000..f41e1d3d9a3 --- /dev/null +++ b/tests/main/app/services/store.js @@ -0,0 +1 @@ +export { default } from 'ember-data/store'; diff --git a/tests/main/app/templates/application.hbs b/tests/main/app/templates/application.hbs index 578920ea827..e2147cab02d 100644 --- a/tests/main/app/templates/application.hbs +++ b/tests/main/app/templates/application.hbs @@ -1,7 +1 @@ -
-

EmberData Graph Tests

- - {{outlet}} - - Tests -
+{{outlet}} \ No newline at end of file diff --git a/tests/main/app/transforms/boolean.js b/tests/main/app/transforms/boolean.js new file mode 100644 index 00000000000..b4d4471fa7f --- /dev/null +++ b/tests/main/app/transforms/boolean.js @@ -0,0 +1 @@ +export { BooleanTransform as default } from '@ember-data/serializer/transform'; diff --git a/tests/main/app/transforms/date.js b/tests/main/app/transforms/date.js new file mode 100644 index 00000000000..4aa235dc618 --- /dev/null +++ b/tests/main/app/transforms/date.js @@ -0,0 +1 @@ +export { DateTransform as default } from '@ember-data/serializer/transform'; diff --git a/tests/main/app/transforms/number.js b/tests/main/app/transforms/number.js new file mode 100644 index 00000000000..47e4c0731a7 --- /dev/null +++ b/tests/main/app/transforms/number.js @@ -0,0 +1 @@ +export { NumberTransform as default } from '@ember-data/serializer/transform'; diff --git a/tests/main/app/transforms/string.js b/tests/main/app/transforms/string.js new file mode 100644 index 00000000000..ba881680d44 --- /dev/null +++ b/tests/main/app/transforms/string.js @@ -0,0 +1 @@ +export { StringTransform as default } from '@ember-data/serializer/transform'; diff --git a/tests/main/bin/calculate-test-jobs b/tests/main/bin/calculate-test-jobs new file mode 100755 index 00000000000..b1d253493dd --- /dev/null +++ b/tests/main/bin/calculate-test-jobs @@ -0,0 +1,21 @@ +#!/bin/bash +script_dir=`dirname "$0"` +file=${script_dir}/../../../failed-test-log.txt + +# If the file exists and is not empty +if [ -s "$file" ]; then + commas=`awk -F "," ' { print NF-1 } ' $file` + count=$((commas + 1)) + # If there aren't enough tests it doesn't really help to parallelize + if (( $count < 48 )); then + echo 1 + exit; + fi +fi + +# $JOBS will be set in CI +if [ -z "$JOBS" ]; then + JOBS=$(sysctl -n hw.ncpu) +fi + +echo $(($JOBS * 1)) diff --git a/tests/main/config/ember-try.js b/tests/main/config/ember-try.js index 71ccac814cf..c248a6e7934 100644 --- a/tests/main/config/ember-try.js +++ b/tests/main/config/ember-try.js @@ -1,4 +1,3 @@ -/* eslint-disable node/no-unpublished-require */ 'use strict'; const getChannelURL = require('ember-source-channel-url'); @@ -24,10 +23,19 @@ module.exports = function () { }, }, }, + { + name: 'ember-lts-4.12', + npm: { + devDependencies: { + 'ember-source': '~4.12.3', + }, + }, + }, { name: 'ember-lts-3.28', npm: { devDependencies: { + 'ember-cli': '~4.12.3', 'ember-source': '~3.28.0', }, }, @@ -37,7 +45,6 @@ module.exports = function () { npm: { devDependencies: { 'ember-source': urls[0], - '@glimmer/component': '^1.1.2', }, }, }, diff --git a/tests/main/config/environment.js b/tests/main/config/environment.js index 05f515f65f6..019a72e058e 100644 --- a/tests/main/config/environment.js +++ b/tests/main/config/environment.js @@ -6,11 +6,10 @@ module.exports = function (environment) { podModulePrefix: 'main-test-app', environment: environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { RAISE_ON_DEPRECATION: false, }, - ASSERT_ALL_DEPRECATIONS: process.env.ASSERT_ALL_DEPRECATIONS === 'true', APP: { // Here you can pass flags/options to your application instance diff --git a/tests/main/ember-cli-build.js b/tests/main/ember-cli-build.js index 93337d7735c..fa7c18ba6c6 100644 --- a/tests/main/ember-cli-build.js +++ b/tests/main/ember-cli-build.js @@ -1,19 +1,20 @@ -/* eslint-disable node/no-unpublished-require */ 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -module.exports = function (defaults) { +module.exports = async function (defaults) { + const { setConfig } = await import('@warp-drive/build-config'); + const { macros } = await import('@warp-drive/build-config/babel-macros'); + const isTest = process.env.EMBER_CLI_TEST_COMMAND; const isProd = process.env.EMBER_ENV === 'production'; - const compatWith = process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null; const terserSettings = { exclude: ['assets/main-test-app.js', 'assets/tests.js', 'assets/test-support.js'], terser: { compress: { - ecma: 2021, + ecma: 2022, passes: 6, // slow, but worth it negate_iife: false, sequences: 30, @@ -30,7 +31,7 @@ module.exports = function (defaults) { }, toplevel: false, sourceMap: false, - ecma: 2021, + ecma: 2022, }, }; @@ -38,50 +39,28 @@ module.exports = function (defaults) { terserSettings.enabled = false; } - let config = { - compatWith, - includeDataAdapterInProduction: true, - includeDataAdapter: true, - debug: { - LOG_PAYLOADS: process.env.DEBUG_DATA ? true : false, - LOG_OPERATIONS: process.env.DEBUG_DATA ? true : false, - LOG_MUTATIONS: process.env.DEBUG_DATA ? true : false, - LOG_NOTIFICATIONS: process.env.DEBUG_DATA ? true : false, - LOG_REQUESTS: process.env.DEBUG_DATA ? true : false, - LOG_REQUEST_STATUS: process.env.DEBUG_DATA ? true : false, - LOG_IDENTIFIERS: process.env.DEBUG_DATA ? true : false, - LOG_GRAPH: process.env.DEBUG_DATA ? true : false, - LOG_INSTANCE_CACHE: process.env.DEBUG_DATA ? true : false, - }, - deprecations: require('@ember-data/private-build-infra/src/deprecations')(compatWith || null), - features: require('@ember-data/private-build-infra/src/features')(isProd), - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }; - let app = new EmberApp(defaults, { - emberData: Object.assign({}, config), + const app = new EmberApp(defaults, { babel: { // this ensures that the same build-time code stripping that is done // for library packages is also done for our tests and dummy app - plugins: [...require('@ember-data/private-build-infra/src/debug-macros')(config)], + plugins: [...macros()], }, 'ember-cli-babel': { throwUnlessParallelizable: true, - includeExternalHelpers: true, enableTypeScriptTransform: true, }, 'ember-cli-terser': terserSettings, - '@embroider/macros': { - // setConfig: { - // '@ember-data/store': { - // polyfillUUID: true, - // }, - // }, - setOwnConfig: config, - }, sourcemaps: { enabled: false, }, }); + setConfig(app, __dirname, { + compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + deprecations: { + DISABLE_6X_DEPRECATIONS: false, + }, + }); + return app.toTree(); }; diff --git a/tests/main/eslint.config.mjs b/tests/main/eslint.config.mjs new file mode 100644 index 00000000000..2a4207df2ff --- /dev/null +++ b/tests/main/eslint.config.mjs @@ -0,0 +1,101 @@ +// @ts-check +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 * as qunit from '@warp-drive/internal-config/eslint/qunit.js'; +import * as js from '@warp-drive/internal-config/eslint/browser.js'; +import * as gts from '@warp-drive/internal-config/eslint/gts.js'; + +const AllowedImports = [ + '@ember/application', + '@ember/array', + '@ember/array/proxy', + '@ember/component', + '@ember/component/helper', + '@ember/controller', + '@ember/object', + '@ember/object/computed', + '@ember/object/mixin', + '@ember/owner', + '@ember/routing/route', + '@ember/runloop', + '@ember/service', + '@ember/test-helpers', + '@ember/test-waiters', + '@glimmer/component', + 'qunit', +]; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js) ================ + js.browser({ + srcDirs: ['app', 'tests'], + allowedImports: AllowedImports, + globals: { gc: true }, + }), + + // browser (js/ts) ================ + typescript.browser({ + srcDirs: ['app', 'tests'], + allowedImports: AllowedImports, + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + }, + }), + + // gts + gts.browser({ + srcDirs: ['app', 'tests'], + allowedImports: AllowedImports, + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + }, + }), + + // files converted to strict must pass these rules before they can be removed from + // the files list here + // see https://github.com/emberjs/data/issues/6233#issuecomment-849279594 + { + files: [ + 'tests/helpers/accessors.ts', + 'tests/integration/identifiers/configuration-test.ts', + 'tests/integration/identifiers/new-records-test.ts', + // 'tests/integration/identifiers/polymorphic-scenarios-test.ts', + 'tests/integration/identifiers/record-identifier-for-test.ts', + 'tests/integration/identifiers/scenarios-test.ts', + 'tests/integration/model-errors-test.ts', + 'tests/integration/record-data/record-data-errors-test.ts', + 'tests/integration/record-data/record-data-state-test.ts', + 'tests/integration/record-data/record-data-test.ts', + 'tests/integration/record-data/store-wrapper-test.ts', + 'tests/integration/relationships/rollback-test.ts', + 'tests/integration/request-state-service-test.ts', + 'tests/unit/custom-class-support/custom-class-model-test.ts', + ], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + }, + }, + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs(), + + // Test Support ================ + qunit.ember({ + allowedImports: AllowedImports, + }), +]; diff --git a/tests/main/package.json b/tests/main/package.json index 8f471eeeedb..f552b9a9232 100644 --- a/tests/main/package.json +++ b/tests/main/package.json @@ -13,9 +13,18 @@ "test": "tests" }, "scripts": { - "build": "ember build", - "test": "ember test --test-port=0", - "test:try-one": "ember try:one" + "build:tests": "IS_TESTING=true EMBER_CLI_TEST_COMMAND=true ember build --output-path=dist-test --suppress-sizes", + "build:production": "pnpm build:tests -e production", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "check:types": "glint", + "examine": "export EXAM_PARALLEL_COUNT=$(./bin/calculate-test-jobs); ember exam --test-port=0 --path=dist-test --parallel=$EXAM_PARALLEL_COUNT --load-balance", + "test:try-one": "ember try:one", + "launch:tests": "ember test --test-port=0 --serve --no-launch", + "start": "bun run build:tests --watch", + "test": "bun run examine", + "test:production": "bun run examine", + "typescript": "^5.2.2", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "author": "", "license": "MIT", @@ -50,72 +59,96 @@ "@ember-data/tracking": { "injected": true }, - "@ember-data/private-build-infra": { + "@ember-data/unpublished-test-infra": { "injected": true }, - "@ember-data/unpublished-test-infra": { + "@warp-drive/holodeck": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/request-utils": { "injected": true } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/plugin-transform-typescript": "^7.21.3", - "@babel/runtime": "^7.21.0", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-typescript": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", "@ember/edition-utils": "^1.2.0", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@embroider/macros": "^1.10.0", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^3.1.1", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", + "@embroider/macros": "^1.16.6", "@glimmer/component": "^1.1.2", - "@glimmer/env": "^0.1.7", "@glimmer/tracking": "^1.1.2", - "@types/ember": "^4.0.3", - "@types/ember-qunit": "^5.0.2", - "@types/ember-testing-helpers": "^0.0.4", - "@types/ember__debug": "^4.0.3", - "@types/ember__object": "^4.0.5", - "@types/ember__utils": "^4.0.2", - "@types/qunit": "^2.19.4", - "@types/rsvp": "^4.0.4", + "@glint/core": "1.5.0", + "@glint/environment-ember-loose": "1.5.0", + "@glint/environment-ember-template-imports": "1.5.0", + "@glint/template": "1.5.0", + "@types/qunit": "2.19.10", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/build-config": "workspace:*", + "@warp-drive/holodeck": "workspace:*", + "@warp-drive/internal-config": "workspace:*", "broccoli-concat": "^4.2.5", "broccoli-merge-trees": "^4.2.0", "broccoli-stew": "^3.0.0", "broccoli-string-replace": "^0.1.2", "broccoli-test-helper": "^2.0.0", "broccoli-uglify-sourcemap": "^4.0.0", - "ember-auto-import": "^2.6.1", - "ember-cached-decorator-polyfill": "^1.0.1", - "ember-strict-resolver": "^1.3.0", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.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", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", "ember-cli-inject-live-reload": "^2.1.0", "ember-cli-terser": "~4.0.2", - "ember-cli-test-loader": "^3.0.0", - "ember-data": "workspace:4.12.8", + "ember-cli-test-loader": "^3.1.0", + "ember-data": "workspace:*", "ember-decorators-polyfill": "^1.1.5", "ember-disable-prototype-extensions": "^1.1.3", - "ember-inflector": "^4.0.2", + "ember-exam": "^9.0.0", + "ember-inflector": "4.0.3", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", + "ember-template-imports": "4.1.3", + "ember-qunit": "8.0.2", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", "ember-source-channel-url": "^3.0.0", - "ember-try": "^2.0.0", + "ember-strict-resolver": "^1.3.0", + "ember-try": "^3.0.0", "loader.js": "^4.7.0", "pretender": "^3.4.7", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "typescript": "~5.0.3", - "webpack": "^5.77.0" + "qunit": "^2.20.1", + "qunit-dom": "^3.1.1", + "typescript": "^5.4.5", + "webpack": "^5.92.0" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "ember": { "edition": "octane" @@ -123,5 +156,8 @@ "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9", + "dependencies": { + "pnpm-sync-dependencies-meta-injected": "0.0.14" + } } diff --git a/tests/main/public/assets/users/list.json b/tests/main/public/assets/users/list.json index 5391c783051..c3ce2908e59 100644 --- a/tests/main/public/assets/users/list.json +++ b/tests/main/public/assets/users/list.json @@ -6,11 +6,13 @@ "expiration": 120000, "total": 1 }, - "data": [{ - "type": "user", - "id": "1", - "attributes": { - "name": "Chris Thoburn" + "data": [ + { + "type": "user", + "id": "1", + "attributes": { + "name": "Chris Thoburn" + } } - }] + ] } diff --git a/tests/main/testem.js b/tests/main/testem.js index c04d87e0ea7..792fa67528a 100644 --- a/tests/main/testem.js +++ b/tests/main/testem.js @@ -1,98 +1,14 @@ -/* eslint-disable node/no-unpublished-require */ -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); +const TestemConfig = require('@ember-data/unpublished-test-infra/testem/testem'); -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); +module.exports = async function () { + const holodeck = (await import('@warp-drive/holodeck')).default; + await holodeck.launchProgram({ + port: 7373, + }); -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - tap_quiet_logs: true, - browser_disconnect_timeout: 45, - browser_start_timeout: 45, - socket_heartbeat_timeout: 75, // test timeout is 60s, so this needs to be longer - browser_reconnect_limit: 10, - socket_server_options: { - maxHttpBufferSize: 10e7, - }, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--no-sandbox', - // '--enable-logging', + process.on('beforeExit', async () => { + await holodeck.endProgram(); + }); - // this may help debug CI in some situations - // '--enable-logging', - - // when debugging memory usage this gives us better data - process.env.DEBUG_MEMORY ? '--enable-precise-memory-info' : false, - process.env.DEBUG_MEMORY ? '--js-flags="--allow-natives-syntax --expose-gc"' : false, - - // these prevent user account - // and extensions from mucking with things - '--incognito', - '--bwsi', - - // On Ubuntu this dev-shm-usage speeds you up on bigger machines - // and slows you down on smaller. We are on a bigger CI box now. - // '--disable-dev-shm-usage', - '--disable-gpu', - '--disable-3d-apis', - '--disable-software-rasterizer', - '--disable-webgl', - '--disable-remote-fonts', - '--blink-settings=imagesEnabled=false', - '--mute-audio', - - // ubuntu-16-core seems to be unhappy with this being set to a non-zero port - // throws: ERROR:socket_posix.cc(147)] bind() failed: Address already in use (98) - '--remote-debugging-port=0', - '--remote-debugging-address=0.0.0.0', - '--window-size=1440,900', - '--proxy-bypass-list=*', - "--proxy-server='direct://'", - ].filter(Boolean), - dev: [ - '--headless', - '--no-sandbox', - - // this may help debug in some situations - // '--enable-logging', - - // when debugging memory usage this gives us better data - process.env.DEBUG_MEMORY ? '--enable-precise-memory-info' : false, - process.env.DEBUG_MEMORY ? '--js-flags="--allow-natives-syntax --expose-gc"' : false, - - // these prevent user account - // and extensions from mucking with things - '--incognito', - '--bwsi', - - // On Ubuntu this dev-shm-usage speeds you up on bigger machines - // and slows you down on smaller. We are on a bigger CI box now. - // '--disable-dev-shm-usage', - '--disable-gpu', - '--disable-3d-apis', - '--disable-software-rasterizer', - '--disable-webgl', - '--disable-remote-fonts', - '--blink-settings=imagesEnabled=false', - '--mute-audio', - - '--remote-debugging-port=9222', - '--remote-debugging-address=0.0.0.0', - '--window-size=1440,900', - '--proxy-bypass-list=*', - "--proxy-server='direct://'", - ].filter(Boolean), - }, - Firefox: { - ci: ['--headless', '--width=1440', '--height=900'], - dev: ['--headless', '--width=1440', '--height=900'], - }, - }, + return TestemConfig; }; diff --git a/tests/main/tests/acceptance/concurrency-test.js b/tests/main/tests/acceptance/concurrency-test.js index fe62a5cb45a..11720edbb56 100644 --- a/tests/main/tests/acceptance/concurrency-test.js +++ b/tests/main/tests/acceptance/concurrency-test.js @@ -186,6 +186,6 @@ module('Acceptance | concurrency', function (hooks) { await settled(); const record1Again = store1.peekRecord('user', '1'); - assert.strictEqual(record1, record1Again, 'record is still in store1'); + assert.strictEqual(record1Again, record1, 'record is still in store1'); }); }); diff --git a/tests/main/tests/acceptance/record-array-test.js b/tests/main/tests/acceptance/record-array-test.js index 0becdca4de1..03e0f5d1305 100644 --- a/tests/main/tests/acceptance/record-array-test.js +++ b/tests/main/tests/acceptance/record-array-test.js @@ -1,19 +1,19 @@ import { computed } from '@ember/object'; import { findAll, render, rerender } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; -import { DEPRECATE_COMPUTED_CHAINS } from '@ember-data/deprecations'; import Model, { attr } from '@ember-data/model'; +import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations'; class Person extends Model { @attr name; } -module('IdentifierArray | Classic Chains', function (hooks) { +module('LiveArray | Classic Chains', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { diff --git a/tests/main/tests/acceptance/relationships/belongs-to-test.js b/tests/main/tests/acceptance/relationships/belongs-to-test.js index 5d7c0f5a70e..8568a201508 100644 --- a/tests/main/tests/acceptance/relationships/belongs-to-test.js +++ b/tests/main/tests/acceptance/relationships/belongs-to-test.js @@ -1,9 +1,8 @@ import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; -import { Promise, resolve } from 'rsvp'; +import { hbs } from 'ember-cli-htmlbars'; import { render as legacyRender } from 'ember-data/test-support'; import { setupRenderingTest } from 'ember-qunit'; @@ -66,7 +65,7 @@ class TestAdapter extends JSONAPIAdapter { return this.pausePromise.then(() => this._nextPayload()); } - let payload = this._payloads.shift(); + const payload = this._payloads.shift(); if (payload === undefined) { this.assert.ok(false, 'Too many adapter requests have been made!'); @@ -99,12 +98,12 @@ class TestAdapter extends JSONAPIAdapter { } deleteRecord() { - return resolve({ data: null }); + return Promise.resolve({ data: null }); } } function makePeopleWithRelationshipData() { - let people = [ + const people = [ { type: 'person', id: '1:no-children-or-parent', @@ -199,7 +198,7 @@ function makePeopleWithRelationshipData() { }, ]; - let peopleHash = {}; + const peopleHash = {}; people.forEach((person) => { peopleHash[person.id] = person; }); @@ -216,7 +215,7 @@ module('async belongs-to rendering tests', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('model:pet', Pet); owner.register('adapter:application', TestAdapter); @@ -234,7 +233,7 @@ module('async belongs-to rendering tests', function (hooks) { module('for local changes', function (hooks) { hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('model:pet', Pet); }); @@ -325,7 +324,7 @@ module('async belongs-to rendering tests', function (hooks) { }); test('async belongsTo returns correct new value after a local change', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { type: 'person', id: '1', @@ -360,8 +359,8 @@ module('async belongs-to rendering tests', function (hooks) { ], }); - let shen = store.peekRecord('pet', '1'); - let pirate = store.peekRecord('pet', '2'); + const shen = store.peekRecord('pet', '1'); + const pirate = store.peekRecord('pet', '2'); let bestDog = await chris.bestDog; this.set('chris', chris); @@ -411,8 +410,8 @@ module('async belongs-to rendering tests', function (hooks) { module('for data-no-link scenarios', function () { test('We can render an async belongs-to', async function (assert) { - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['5:has-parent-no-children'], }); @@ -429,8 +428,8 @@ module('async belongs-to rendering tests', function (hooks) { }); test('We can delete an async belongs-to', async function (assert) { - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['5:has-parent-no-children'], }); @@ -443,9 +442,9 @@ module('async belongs-to rendering tests', function (hooks) {

{{this.sedona.parent.name}}

`); - let parent = await sedona.parent; + const parent = await sedona.parent; await parent.destroyRecord(); - let newParent = await sedona.parent; + const newParent = await sedona.parent; assert.strictEqual(newParent, null, 'We no longer have a parent'); assert.strictEqual( @@ -456,8 +455,8 @@ module('async belongs-to rendering tests', function (hooks) { }); test('Re-rendering an async belongsTo does not cause a new fetch', async function (assert) { - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['5:has-parent-no-children'], }); @@ -480,8 +479,8 @@ module('async belongs-to rendering tests', function (hooks) { }); test('Rendering an async belongs-to whose fetch fails does not trigger a new request', async function (assert) { - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['5:has-parent-no-children'], }); @@ -513,7 +512,7 @@ module('async belongs-to rendering tests', function (hooks) { // Here we assign our handler to the corresponding global, window property window.addEventListener('unhandledrejection', globalPromiseRejectionHandler, true); - let originalPushResult = assert.pushResult; + const originalPushResult = assert.pushResult; assert.pushResult = function (result) { if ( result.result === false && @@ -532,8 +531,8 @@ module('async belongs-to rendering tests', function (hooks) { const relationship = sedona.belongsTo('parent').belongsToRelationship; const { state, definition } = relationship; - let RelationshipPromiseCache = LEGACY_SUPPORT.get(sedona)._relationshipPromisesCache; - let RelationshipProxyCache = LEGACY_SUPPORT.get(sedona)._relationshipProxyCache; + const RelationshipPromiseCache = LEGACY_SUPPORT.get(sedona)._relationshipPromisesCache; + const RelationshipProxyCache = LEGACY_SUPPORT.get(sedona)._relationshipProxyCache; assert.true(definition.isAsync, 'The relationship is async'); assert.false(state.isEmpty, 'The relationship is not empty'); @@ -546,7 +545,7 @@ module('async belongs-to rendering tests', function (hooks) { assert.false(!!relationship.link, 'The relationship does not have a link'); try { - let result = sedona.parent.content; + const result = sedona.parent.content; assert.strictEqual(result, null, 're-access is safe'); } catch (e) { assert.ok(false, `Re-accessing unexpectedly resulted in rejected promise error: ${e.message}`); @@ -565,8 +564,8 @@ module('async belongs-to rendering tests', function (hooks) { test('accessing a linked async belongs-to whose fetch fails does not error for null proxy content', async function (assert) { assert.expect(3); - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['6:has-linked-parent'], }); @@ -597,8 +596,8 @@ module('async belongs-to rendering tests', function (hooks) { test('Can reset a previously failed linked async belongs-to', async function (assert) { assert.expect(5); - let people = makePeopleWithRelationshipData(); - let sedona = store.push({ + const people = makePeopleWithRelationshipData(); + const sedona = store.push({ data: people.dict['6:has-linked-parent'], }); @@ -616,7 +615,7 @@ module('async belongs-to rendering tests', function (hooks) { // Here we assign our handler to the corresponding global, window property window.addEventListener('unhandledrejection', globalPromiseRejectionHandler, true); - let originalPushResult = assert.pushResult; + const originalPushResult = assert.pushResult; assert.pushResult = function (result) { if (result.result === false && result.message === 'global failure: Error: person not found') { return; diff --git a/tests/main/tests/acceptance/relationships/has-many-test.js b/tests/main/tests/acceptance/relationships/has-many-test.js index a7df92aad16..7bd2f103bbb 100644 --- a/tests/main/tests/acceptance/relationships/has-many-test.js +++ b/tests/main/tests/acceptance/relationships/has-many-test.js @@ -1,24 +1,24 @@ import ArrayProxy from '@ember/array/proxy'; +import { setComponentTemplate } from '@ember/component'; import { action } from '@ember/object'; import { sort } from '@ember/object/computed'; import { inject as service } from '@ember/service'; -import { click, find, findAll, render, rerender } from '@ember/test-helpers'; +import { click, find, findAll, render, rerender, settled } from '@ember/test-helpers'; import Component from '@glimmer/component'; -import hbs from 'htmlbars-inline-precompile'; -import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; +import QUnit, { module, test } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; import { render as legacyRender } from 'ember-data/test-support'; import { setupRenderingTest } from 'ember-qunit'; import { ServerError } from '@ember-data/adapter/error'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import { DEPRECATE_ARRAY_LIKE } from '@ember-data/deprecations'; 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() @@ -57,17 +57,17 @@ class TestAdapter extends JSONAPIAdapter { if (this.isPaused) { return this.pausePromise.then(() => this._nextPayload()); } - let payload = this._payloads.shift(); + const payload = this._payloads.shift(); if (payload === undefined) { this.assert.ok(false, 'Too many adapter requests have been made!'); - return resolve({ data: null }); + return Promise.resolve({ data: null }); } if (payload instanceof ServerError) { - return reject(payload); + return Promise.reject(payload); } - return resolve(payload); + return Promise.resolve(payload); } // find by link @@ -87,7 +87,7 @@ class TestAdapter extends JSONAPIAdapter { } function makePeopleWithRelationshipData() { - let people = [ + const people = [ { type: 'person', id: '1:no-children-or-parent', @@ -169,7 +169,7 @@ function makePeopleWithRelationshipData() { }, ]; - let peopleHash = {}; + const peopleHash = {}; people.forEach((person) => { peopleHash[person.id] = person; }); @@ -181,12 +181,12 @@ function makePeopleWithRelationshipData() { } function makePeopleWithRelationshipLinks(removeData = true) { - let people = makePeopleWithRelationshipData(); - let linkPayloads = (people.links = {}); + const people = makePeopleWithRelationshipData(); + const linkPayloads = (people.links = {}); people.all.map((person) => { Object.keys(person.relationships).forEach((relName) => { - let rel = person.relationships[relName]; + const rel = person.relationships[relName]; let data = rel.data; if (removeData === true) { @@ -214,23 +214,21 @@ function makePeopleWithRelationshipLinks(removeData = true) { } module('async has-many rendering tests', function (hooks) { - let store; - let adapter; setupRenderingTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('adapter:application', TestAdapter); owner.register('serializer:application', JSONAPISerializer); - store = owner.lookup('service:store'); - adapter = store.adapterFor('application'); }); module('for data-no-link scenarios', function () { test('We can render an async hasMany', async function (assert) { - let people = makePeopleWithRelationshipData(); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipData(); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -250,14 +248,16 @@ module('async has-many rendering tests', function (hooks) { `); - let names = findAll('li').map((e) => e.textContent); + const names = findAll('li').map((e) => e.textContent); assert.deepEqual(names, ['Selena has a parent', 'Sedona has a parent'], 'We rendered the names'); }); test('Re-rendering an async hasMany does not cause a new fetch', async function (assert) { - let people = makePeopleWithRelationshipData(); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipData(); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -295,23 +295,53 @@ module('async has-many rendering tests', function (hooks) { }); test('Rendering an async hasMany whose fetch fails does not trigger a new request', async function (assert) { - assert.expect(11); - let people = makePeopleWithRelationshipData(); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const people = makePeopleWithRelationshipData(); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); + let hasFired = false; - adapter.setupPayloads(assert, [ - { data: people.dict['4:has-parent-no-children'] }, - new ServerError([], 'hard error while finding 5:has-parent-no-children'), - ]); + class TestAdapter { + static create() { + return new this(); + } + + shouldReloadRecord() { + return false; + } + + shouldBackgroundReloadRecord() { + return false; + } + + findRecord(_store, schema, id) { + assert.step(`findRecord ${schema.modelName} ${id}`); + + if (id === '4:has-parent-no-children') { + return Promise.resolve({ data: people.dict['4:has-parent-no-children'] }); + } + if (hasFired) { + assert.ok(false, 'We only reject a single time'); + // prevent further recursive calls + return Promise.resolve({ data: people.dict['5:has-parent-no-children'] }); + } + // slight delay helps ensure we are less flakey + return new Promise((_res, rej) => { + setTimeout(() => { + rej(new Error('hard error while finding 5:has-parent-no-children')); + }, 5); + }); + } + } + this.owner.register('adapter:application', TestAdapter); // render this.set('parent', parent); - let hasFired = false; // This function handles any unhandled promise rejections const globalPromiseRejectionHandler = (event) => { + assert.step('unhandledrejection'); if (!hasFired) { hasFired = true; assert.ok(true, 'Children promise did reject'); @@ -322,7 +352,7 @@ module('async has-many rendering tests', function (hooks) { ); } else { assert.ok(false, 'We only reject a single time'); - adapter.pause(); // prevent further recursive calls to load the relationship + // adapter.pause(); // prevent further recursive calls to load the relationship } event.preventDefault(); return false; @@ -330,18 +360,16 @@ module('async has-many rendering tests', function (hooks) { // Here we assign our handler to the corresponding global, window property window.addEventListener('unhandledrejection', globalPromiseRejectionHandler, true); - let originalPushResult = assert.pushResult; - assert.pushResult = function (result) { - if ( - result.result === false && - result.message === 'global failure: Error: hard error while finding 5:has-parent-no-children' - ) { - return; + const onUncaughtException = QUnit.onUncaughtException; + QUnit.onUncaughtException = function (error) { + assert.step('onUncaughtException'); + assert.strictEqual(error.message, 'hard error while finding 5:has-parent-no-children'); + if (error.message !== 'hard error while finding 5:has-parent-no-children') { + onUncaughtException.call(this, error); } - return originalPushResult.call(this, result); }; - await legacyRender(hbs` + await render(hbs`
    {{#each this.parent.children as |child|}}
  • {{child.name}}
  • @@ -349,13 +377,23 @@ module('async has-many rendering tests', function (hooks) {
`); - let names = findAll('li').map((e) => e.textContent); + await store._getAllPending(); + await settled(); - assert.deepEqual(names, ['Selena has a parent'], 'We rendered only the names for successful requests'); + assert.verifySteps([ + 'findRecord person 4:has-parent-no-children', + 'findRecord person 5:has-parent-no-children', + 'onUncaughtException', + 'unhandledrejection', + ]); - let relationshipState = parent.hasMany('children').hasManyRelationship; - let RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; - let RelationshipProxyCache = LEGACY_SUPPORT.get(parent)._relationshipProxyCache; + const names = findAll('li').map((e) => e.textContent); + + assert.arrayStrictEquals(names, ['Selena has a parent'], 'We rendered only the names for successful requests'); + + const relationshipState = parent.hasMany('children').hasManyRelationship; + const RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; + const RelationshipProxyCache = LEGACY_SUPPORT.get(parent)._relationshipProxyCache; assert.true(relationshipState.definition.isAsync, 'The relationship is async'); assert.false(relationshipState.state.isEmpty, 'The relationship is not empty'); @@ -366,15 +404,21 @@ module('async has-many rendering tests', function (hooks) { assert.true(!!RelationshipProxyCache['children'], 'The relationship has a promise proxy'); assert.false(!!relationshipState.link, 'The relationship does not have a link'); + await rerender(); + + assert.verifySteps([]); + window.removeEventListener('unhandledrejection', globalPromiseRejectionHandler, true); - assert.pushResult = originalPushResult; + QUnit.onUncaughtException = onUncaughtException; }); }); module('for link-no-data scenarios', function () { test('We can render an async hasMany with a link', async function (assert) { - let people = makePeopleWithRelationshipLinks(true); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipLinks(true); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -391,14 +435,16 @@ module('async has-many rendering tests', function (hooks) { `); - let names = findAll('li').map((e) => e.textContent); + const names = findAll('li').map((e) => e.textContent); assert.deepEqual(names, ['Selena has a parent', 'Sedona has a parent'], 'We rendered the names'); }); test('Re-rendering an async hasMany with a link does not cause a new fetch', async function (assert) { - let people = makePeopleWithRelationshipLinks(true); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipLinks(true); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -433,9 +479,11 @@ module('async has-many rendering tests', function (hooks) { }); test('Rendering an async hasMany with a link whose fetch fails does not trigger a new request', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); assert.expect(11); - let people = makePeopleWithRelationshipLinks(true); - let parent = store.push({ + const people = makePeopleWithRelationshipLinks(true); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -469,7 +517,7 @@ module('async has-many rendering tests', function (hooks) { // Here we assign our handler to the corresponding global, window property window.addEventListener('unhandledrejection', globalPromiseRejectionHandler, true); - let originalPushResult = assert.pushResult; + const originalPushResult = assert.pushResult; assert.pushResult = function (result) { if ( result.result === false && @@ -489,13 +537,13 @@ module('async has-many rendering tests', function (hooks) { `); - let names = findAll('li').map((e) => e.textContent); + const names = findAll('li').map((e) => e.textContent); assert.deepEqual(names, [], 'We rendered no names'); - let relationshipState = parent.hasMany('children').hasManyRelationship; - let RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; - let RelationshipProxyCache = LEGACY_SUPPORT.get(parent)._relationshipProxyCache; + const relationshipState = parent.hasMany('children').hasManyRelationship; + const RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; + const RelationshipProxyCache = LEGACY_SUPPORT.get(parent)._relationshipProxyCache; assert.true(relationshipState.definition.isAsync, 'The relationship is async'); assert.true( @@ -516,8 +564,10 @@ module('async has-many rendering tests', function (hooks) { module('for link-and-data scenarios', function () { test('We can render an async hasMany with a link and data', async function (assert) { - let people = makePeopleWithRelationshipLinks(false); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipLinks(false); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -534,14 +584,16 @@ module('async has-many rendering tests', function (hooks) { `); - let names = findAll('li').map((e) => e.textContent); + const names = findAll('li').map((e) => e.textContent); assert.deepEqual(names, ['Selena has a parent', 'Sedona has a parent'], 'We rendered the names'); }); test('Rendering an async hasMany with a link and data where data has been side-loaded does not fetch the link', async function (assert) { - let people = makePeopleWithRelationshipLinks(false); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipLinks(false); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], included: [people.dict['4:has-parent-no-children'], people.dict['5:has-parent-no-children']], }); @@ -560,14 +612,16 @@ module('async has-many rendering tests', function (hooks) { `); - let names = findAll('li').map((e) => e.textContent); + const names = findAll('li').map((e) => e.textContent); assert.deepEqual(names, ['Selena has a parent', 'Sedona has a parent'], 'We rendered the names'); }); test('Re-rendering an async hasMany with a link and data does not cause a new fetch', async function (assert) { - let people = makePeopleWithRelationshipLinks(false); - let parent = store.push({ + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const people = makePeopleWithRelationshipLinks(false); + const parent = store.push({ data: people.dict['3:has-2-children-and-parent'], }); @@ -717,8 +771,7 @@ module('autotracking through ArrayProxy', function (hooks) { {{/each}} `; - owner.register('component:person-overview', PersonOverview); - owner.register('template:components/person-overview', layout); + owner.register('component:person-overview', setComponentTemplate(layout, PersonOverview)); this.set('person', chris); await render(hbs``); assert.strictEqual(find('#comments-count').textContent, 'Comments (3)', 'We have the right comments count'); @@ -752,7 +805,7 @@ module('autotracking has-many', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('adapter:application', TestAdapter); owner.register('serializer:application', JSONAPISerializer); @@ -769,7 +822,7 @@ module('autotracking has-many', function (hooks) { get sortedChildren() { if (DEPRECATE_ARRAY_LIKE) { - let result = this.children.sortBy('name'); + const result = this.children.sortBy('name'); assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); return result; } else { @@ -785,7 +838,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.sortedChildren.length}}

@@ -795,12 +848,11 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); - let person = store.peekRecord('person', '1'); - let children = await person.children; + const person = store.peekRecord('person', '1'); + const children = await person.children; this.model = { person, children }; await render(hbs``); @@ -822,13 +874,13 @@ module('autotracking has-many', function (hooks) { deprecatedTest( 'We can re-render hasMany w/PromiseManyArray.sortBy', - { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, async function (assert) { class ChildrenList extends Component { @service store; get sortedChildren() { - let result = this.args.person.children.sortBy('name'); + const result = this.args.person.children.sortBy('name'); assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); return result; } @@ -841,7 +893,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.sortedChildren.length}}

@@ -851,8 +903,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -877,7 +928,7 @@ module('autotracking has-many', function (hooks) { deprecatedTest( 'We can re-render hasMany with sort computed macro on PromiseManyArray', - { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, async function (assert) { class ChildrenList extends Component { @service store; @@ -893,7 +944,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.sortedChildren.length}}

@@ -903,8 +954,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -924,13 +974,13 @@ module('autotracking has-many', function (hooks) { 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: 6 }); + 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: 12 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, async function (assert) { let calls = 0; class ChildrenList extends Component { @@ -956,14 +1006,13 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.firstChild.name}}

{{this.lastChild.name}}

`; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -986,7 +1035,7 @@ module('autotracking has-many', function (hooks) { deprecatedTest( 'We can re-render hasMany with PromiseManyArray.map', - { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 6 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, async function (assert) { class ChildrenList extends Component { @service store; @@ -1003,7 +1052,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.children.length}}

@@ -1013,8 +1062,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1049,7 +1097,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{@person.children.length}}

@@ -1059,8 +1107,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1097,7 +1144,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.sortedChildren.length}}

@@ -1107,8 +1154,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1147,13 +1193,12 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.firstChild.name}}

`; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1188,7 +1233,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.children.length}}

@@ -1198,8 +1243,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1238,7 +1282,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.children.length}}

@@ -1248,8 +1292,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); store.createRecord('person', { id: '1', name: 'Doodad' }); this.person = store.peekRecord('person', '1'); @@ -1303,7 +1346,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.children.length}}

@@ -1313,8 +1356,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); this.person = store.push({ data: { @@ -1388,7 +1430,7 @@ module('autotracking has-many', function (hooks) { } } - let layout = hbs` + const layout = hbs`

{{this.allPeople.length}}

@@ -1398,8 +1440,7 @@ module('autotracking has-many', function (hooks) { {{/each}} `; - this.owner.register('component:people-list', PeopleList); - this.owner.register('template:components/people-list', layout); + this.owner.register('component:people-list', setComponentTemplate(layout, PeopleList)); store.createRecord('person', { id: '1', name: 'Doodad' }); diff --git a/tests/main/tests/acceptance/relationships/tracking-record-state-test.js b/tests/main/tests/acceptance/relationships/tracking-record-state-test.js index 1bdbcc6779b..a558f55c1af 100644 --- a/tests/main/tests/acceptance/relationships/tracking-record-state-test.js +++ b/tests/main/tests/acceptance/relationships/tracking-record-state-test.js @@ -1,13 +1,14 @@ +import { setComponentTemplate } from '@ember/component'; import { action } from '@ember/object'; import { inject } from '@ember/service'; import { click, findAll, render } from '@ember/test-helpers'; import Component from '@glimmer/component'; +// eslint-disable-next-line no-restricted-imports import { tracked } from '@glimmer/tracking'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; @@ -32,7 +33,7 @@ module('tracking state flags on a record', function (hooks) { tag.rev; // subscribe if (_isDirty && !_isUpdating) { _isUpdating = true; - resolve(desc.get.call(this)).then((v) => { + Promise.resolve(desc.get.call(this)).then((v) => { _value = v; _isDirty = false; tag.rev++; @@ -128,7 +129,7 @@ module('tracking state flags on a record', function (hooks) { class Adapter { createRecord() { assert.ok(true, 'createRecord was called to save'); - return resolve({ data: { type: 'person', id: `${serverId++}` } }); + return Promise.resolve({ data: { type: 'person', id: `${serverId++}` } }); } static create() { return new this(); @@ -136,8 +137,7 @@ module('tracking state flags on a record', function (hooks) { } this.owner.register('model:person', Person); - this.owner.register('component:children-list', ChildrenList); - this.owner.register('template:components/children-list', layout); + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); this.owner.register('serializer:application', Serializer); this.owner.register('adapter:application', Adapter); const store = this.owner.lookup('service:store'); diff --git a/tests/main/tests/acceptance/tracking-create-record-test.js b/tests/main/tests/acceptance/tracking-create-record-test.js index 7a11f67c21f..09dea7f66bc 100644 --- a/tests/main/tests/acceptance/tracking-create-record-test.js +++ b/tests/main/tests/acceptance/tracking-create-record-test.js @@ -1,15 +1,17 @@ +import { setComponentTemplate } from '@ember/component'; import { inject as service } from '@ember/service'; import { render, settled } from '@ember/test-helpers'; import Component from '@glimmer/component'; -import { cached, tracked } from '@glimmer/tracking'; +// eslint-disable-next-line no-restricted-imports +import { tracked } from '@glimmer/tracking'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; -import { memoTransact, transact, untracked } from '@ember-data/tracking'; +import { cached, memoTransact, transact, untracked } from '@ember-data/tracking'; module('acceptance/tracking-transactions', function (hooks) { setupRenderingTest(hooks); @@ -31,7 +33,7 @@ module('acceptance/tracking-transactions', function (hooks) { const records = arr.filter((r) => r.isNew); if (records.length === 0) { // invalidate length - let record = this.store.createRecord('widget', { name: 'Chris' }); + const record = this.store.createRecord('widget', { name: 'Chris' }); records.push(record); } return records; @@ -39,7 +41,7 @@ module('acceptance/tracking-transactions', function (hooks) { } } - let layout = hbs` + const layout = hbs`
    {{#each this.widgets as |widget|}}
  • {{widget.name}} {{if widget.isValid 'Is Valid' 'Is Invalid'}}
  • @@ -48,8 +50,7 @@ module('acceptance/tracking-transactions', function (hooks) { `; owner.register('model:widget', Widget); - owner.register('component:widget-creator', WidgetCreator); - owner.register('template:components/widget-creator', layout); + owner.register('component:widget-creator', setComponentTemplate(layout, WidgetCreator)); const store = owner.lookup('service:store'); await render(hbs` @@ -83,7 +84,7 @@ module('acceptance/tracking-transactions', function (hooks) { const records = arr.filter((r) => r.isNew); if (records.length === 0) { // invalidate length - let record = this.store.createRecord('widget', { name }); + const record = this.store.createRecord('widget', { name }); records.push(record); } return records; @@ -95,7 +96,7 @@ module('acceptance/tracking-transactions', function (hooks) { } } - let layout = hbs` + const layout = hbs`
      {{#each this.widgets as |widget|}}
    • {{widget.name}} {{if widget.isValid 'Is Valid' 'Is Invalid'}}
    • @@ -104,8 +105,7 @@ module('acceptance/tracking-transactions', function (hooks) { `; owner.register('model:widget', Widget); - owner.register('component:widget-creator', WidgetCreator); - owner.register('template:components/widget-creator', layout); + owner.register('component:widget-creator', setComponentTemplate(layout, WidgetCreator)); const store = owner.lookup('service:store'); await render(hbs` @@ -174,7 +174,7 @@ module('acceptance/tracking-transactions', function (hooks) { } } - let layout = hbs` + const layout = hbs`
        {{#each this.widgets.data as |widget|}}
      • {{widget.name}} {{if widget.isValid 'Is Valid' 'Is Invalid'}}
      • @@ -183,8 +183,7 @@ module('acceptance/tracking-transactions', function (hooks) { `; owner.register('model:widget', Widget); - owner.register('component:widget-creator', WidgetCreator); - owner.register('template:components/widget-creator', layout); + owner.register('component:widget-creator', setComponentTemplate(layout, WidgetCreator)); this.name = 'Chris'; await render(hbs` diff --git a/tests/main/tests/acceptance/tracking-model-id-test.js b/tests/main/tests/acceptance/tracking-model-id-test.js index 005eb8453c3..16ff631f30c 100644 --- a/tests/main/tests/acceptance/tracking-model-id-test.js +++ b/tests/main/tests/acceptance/tracking-model-id-test.js @@ -1,10 +1,10 @@ +import { setComponentTemplate } from '@ember/component'; import { render, settled } from '@ember/test-helpers'; import Component from '@glimmer/component'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -21,13 +21,13 @@ class Widget extends Model { class WidgetList extends Component { get sortedWidgets() { - let { widgets } = this.args; + const { widgets } = this.args; return widgets.slice().sort((a, b) => b.numericId - a.numericId); } } -let layout = hbs` +const layout = hbs`
          {{#each this.sortedWidgets as |widget index|}}
        • @@ -42,7 +42,7 @@ let layout = hbs` class TestAdapter extends JSONAPIAdapter { createRecord() { - return resolve({ + return Promise.resolve({ data: { id: '4', type: 'widget', @@ -58,16 +58,15 @@ module('acceptance/tracking-model-id - tracking model id', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:widget', Widget); - owner.register('component:widget-list', WidgetList); - owner.register('template:components/widget-list', layout); + owner.register('component:widget-list', setComponentTemplate(layout, WidgetList)); owner.register('adapter:application', TestAdapter); owner.register('serializer:application', JSONAPISerializer); }); test("can track model id's without using get", async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.createRecord('widget', { id: '1', name: 'Doodad' }); store.createRecord('widget', { id: '3', name: 'Gizmo' }); store.createRecord('widget', { id: '2', name: 'Gadget' }); @@ -83,7 +82,7 @@ module('acceptance/tracking-model-id - tracking model id', function (hooks) { assert.dom('ul>li.widget1>div.name').containsText('Gadget'); assert.dom('ul>li.widget2>div.name').containsText('Doodad'); - let contraption = store.createRecord('widget', { name: 'Contraption' }); + const contraption = store.createRecord('widget', { name: 'Contraption' }); await contraption.save(); await settled(); diff --git a/tests/main/tests/acceptance/tracking-promise-flags-test.js b/tests/main/tests/acceptance/tracking-promise-flags-test.js index 50187219dda..f3ae8ec1117 100644 --- a/tests/main/tests/acceptance/tracking-promise-flags-test.js +++ b/tests/main/tests/acceptance/tracking-promise-flags-test.js @@ -1,8 +1,8 @@ import { render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; import { module } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -17,7 +17,7 @@ module('acceptance/tracking-promise-flags', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:widget', Widget); owner.register( 'serializer:application', @@ -44,7 +44,7 @@ module('acceptance/tracking-promise-flags', function (hooks) { } } owner.register('adapter:application', TestAdapter); - let store = owner.lookup('service:store'); + const store = owner.lookup('service:store'); store.DISABLE_WAITER = true; this.model = store.findRecord('widget', '1'); diff --git a/tests/main/tests/end-user-dx/meaningful-backtracking-errors-test.js b/tests/main/tests/end-user-dx/meaningful-backtracking-errors-test.js index e24968a051b..e651acd10c0 100644 --- a/tests/main/tests/end-user-dx/meaningful-backtracking-errors-test.js +++ b/tests/main/tests/end-user-dx/meaningful-backtracking-errors-test.js @@ -40,7 +40,7 @@ module('DX | Meaningful Backtracking Errors', function (hooks) { error.message.includes( 'You attempted to update .length, but it had already been used previously in the same computation' ), - 'we have a meaningful error' + `we have a meaningful error: ${error.message}` ); return false; } diff --git a/tests/main/tests/helpers/accessors.ts b/tests/main/tests/helpers/accessors.ts index 063b551a9f4..903f0282925 100644 --- a/tests/main/tests/helpers/accessors.ts +++ b/tests/main/tests/helpers/accessors.ts @@ -1,17 +1,11 @@ +import type { GraphEdge, ImplicitEdge } from '@ember-data/graph/-private'; import { graphFor } from '@ember-data/graph/-private'; -import type { ImplicitRelationship } from '@ember-data/graph/-private/graph'; -import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; -import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { ConfidentDict as RelationshipDict } from '@ember-data/types/q/utils'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; -export function getRelationshipStateForRecord( - record: { store: Store }, - propertyName: string -): BelongsToRelationship | ManyRelationship | ImplicitRelationship { +export function getRelationshipStateForRecord(record: { store: Store }, propertyName: string): GraphEdge { const identifier = recordIdentifierFor(record); return graphFor(record.store).get(identifier, propertyName); } @@ -28,16 +22,16 @@ export function hasRelationshipForRecord( } export function implicitRelationshipsFor( - storeWrapper: CacheStoreWrapper, + storeWrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier -): RelationshipDict { +): { [key: string]: ImplicitEdge } { const rels = graphFor(storeWrapper).identifiers.get(identifier); if (!rels) { throw new Error(`Expected at least one relationship to be populated`); } - let implicits = Object.create(null); + const implicits = Object.create(null); Object.keys(rels).forEach((key) => { - let rel = rels[key]!; + const rel = rels[key]; if (rel.definition.isImplicit) { implicits[key] = rel; } diff --git a/tests/main/tests/helpers/create-tracking-context.gjs b/tests/main/tests/helpers/create-tracking-context.gjs new file mode 100644 index 00000000000..379ebd907c0 --- /dev/null +++ b/tests/main/tests/helpers/create-tracking-context.gjs @@ -0,0 +1,53 @@ +import { render, settled } from '@ember/test-helpers'; +import Component from '@glimmer/component'; +// eslint-disable-next-line no-restricted-imports +import { tracked } from '@glimmer/tracking'; + +// eslint-disable-next-line @typescript-eslint/require-await +export default async function createTrackingContext(props) { + let instance; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const testKeys = Object.keys(props); + class TestComponent extends Component { + @tracked count = 1; + + constructor() { + super(...arguments); + // eslint-disable-next-line @typescript-eslint/no-this-alias + instance = this; + } + + get ___value() { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.count; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return testKeys.map((key) => this[key]); + } + + + } + + const defs = {}; + testKeys.forEach((key) => (defs[key] = Object.getOwnPropertyDescriptor(props, key))); + + Object.defineProperties(TestComponent.prototype, defs); + + async function initialRender() { + await render(); + } + + return { + async render() { + if (!instance) { + await initialRender(); + await settled(); + } else { + instance.count++; + await settled(); + } + }, + instance, + }; +} diff --git a/tests/main/tests/helpers/create-tracking-context.js b/tests/main/tests/helpers/create-tracking-context.js deleted file mode 100644 index 152e9a4c321..00000000000 --- a/tests/main/tests/helpers/create-tracking-context.js +++ /dev/null @@ -1,51 +0,0 @@ -import { render, settled } from '@ember/test-helpers'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -import hbs from 'htmlbars-inline-precompile'; - -export default async function createTrackingContext(owner, props) { - let instance; - let testKeys = Object.keys(props); - class TestComponent extends Component { - @tracked count = 1; - - constructor() { - super(...arguments); - instance = this; - } - - get ___value() { - this.count; - return testKeys.map((key) => this[key]); - } - } - - let defs = {}; - testKeys.forEach((key) => (defs[key] = Object.getOwnPropertyDescriptor(props, key))); - - Object.defineProperties(TestComponent.prototype, defs); - - owner.register('component:test-component', TestComponent); - owner.register( - 'template:components/test-component', - hbs`
          {{this.count}}
            {{#each this.___value as |prop|}}
          • {{prop}}
          • {{/each}}
          ` - ); - - async function initialRender() { - await render(hbs``); - } - - return { - async render() { - if (!instance) { - await initialRender(); - await settled(); - } else { - instance.count++; - await settled(); - } - }, - instance, - }; -} diff --git a/tests/main/tests/helpers/reactive-context.gts b/tests/main/tests/helpers/reactive-context.gts new file mode 100644 index 00000000000..fc8881f6c9b --- /dev/null +++ b/tests/main/tests/helpers/reactive-context.gts @@ -0,0 +1,82 @@ +import { get } from '@ember/helper'; +import type { TestContext } from '@ember/test-helpers'; +import { render } from '@ember/test-helpers'; +import Component from '@glimmer/component'; + +import type Model from '@ember-data/model'; +import type { FieldSchema, IdentityField, ResourceSchema } from '@warp-drive/core-types/schema/fields'; + +export interface ReactiveContext { + counters: Record; + fieldOrder: string[]; + reset: () => void; +} + +export async function reactiveContext( + this: TestContext, + record: T, + resource: ResourceSchema +): Promise { + const _fields: string[] = []; + const fields: Array = resource.fields.slice(); + if (resource.identity?.name) { + fields.unshift(resource.identity as IdentityField); + } + fields.forEach((field) => { + _fields.push(field.name + 'Count'); + _fields.push(field.name); + }); + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface ReactiveComponent extends Record {} + class ReactiveComponent extends Component { + get __allFields() { + return _fields as unknown as string; + } + + + } + const counters: Record = {}; + + fields.forEach((field) => { + counters[field.name] = 0; + Object.defineProperty(ReactiveComponent.prototype, field.name + 'Count', { + get() { + return counters[field.name]; + }, + }); + Object.defineProperty(ReactiveComponent.prototype, field.name, { + get() { + counters[field.name]++; + switch (field.kind) { + case 'hasMany': + return `[${(record[field.name as keyof T] as Model[]).map((r) => r.id).join(',')}]`; + case 'belongsTo': + return (record[field.name as keyof T] as Model).id; + case 'field': + return record[field.name as keyof T] as unknown; + default: + throw new Error(`Unknown field kind ${field.kind} for field ${field.name}`); + } + }, + }); + }); + + await render(); + + function reset() { + fields.forEach((field) => { + counters[field.name] = 0; + }); + } + + return { counters, reset, fieldOrder: _fields }; +} diff --git a/tests/main/tests/helpers/reactive-context.ts b/tests/main/tests/helpers/reactive-context.ts deleted file mode 100644 index 6ef15a947f1..00000000000 --- a/tests/main/tests/helpers/reactive-context.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { TestContext } from '@ember/test-helpers'; -import { render } from '@ember/test-helpers'; -import Component from '@glimmer/component'; - -import { hbs } from 'ember-cli-htmlbars'; - -import type Model from '@ember-data/model'; - -export interface ReactiveContext { - counters: Record; - fieldOrder: string[]; - reset: () => void; -} - -export async function reactiveContext( - this: TestContext, - record: T, - fields: { name: string; type: 'field' | 'hasMany' | 'belongsTo' }[] -): Promise { - const _fields: string[] = []; - fields.forEach((field) => { - _fields.push(field.name + 'Count'); - _fields.push(field.name); - }); - - class ReactiveComponent extends Component { - get __allFields() { - return _fields; - } - } - const counters: Record = {}; - - fields.forEach((field) => { - counters[field.name] = 0; - Object.defineProperty(ReactiveComponent.prototype, field.name + 'Count', { - get() { - return counters[field.name]; - }, - }); - Object.defineProperty(ReactiveComponent.prototype, field.name, { - get() { - counters[field.name]++; - switch (field.type) { - case 'hasMany': - return `[${(record[field.name as keyof T] as Model[]).map((r) => r.id as string).join(',')}]`; - case 'belongsTo': - return (record[field.name as keyof T] as Model).id; - case 'field': - return record[field.name as keyof T] as unknown; - default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Unknown field type ${field.type} for field ${field.name}`); - } - }, - }); - }); - - this.owner.register('component:reactive-component', ReactiveComponent); - this.owner.register( - 'template:components/reactive-component', - hbs`
            {{#each this.__allFields as |prop|}}
          • {{prop}}: {{get this prop}}
          • {{/each}}
          ` - ); - - await render(hbs``); - - function reset() { - fields.forEach((field) => { - counters[field.name] = 0; - }); - } - - return { counters, reset, fieldOrder: _fields }; -} diff --git a/tests/main/tests/helpers/watch-property.js b/tests/main/tests/helpers/watch-property.js index 2a4bebd4792..35ced70ad1e 100644 --- a/tests/main/tests/helpers/watch-property.js +++ b/tests/main/tests/helpers/watch-property.js @@ -2,9 +2,10 @@ import { helper } from '@ember/component/helper'; import { addObserver, removeObserver } from '@ember/object/observers'; import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; import QUnit from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; + function freeze(obj) { if (typeof Object.freeze === 'function') { return Object.freeze(obj); @@ -38,7 +39,7 @@ function makeCounter() { } export function watchProperty(obj, propertyName) { - let { counter, increment } = makeCounter(); + const { counter, increment } = makeCounter(); function observe() { increment(); @@ -71,21 +72,21 @@ export async function startWatching() { } export function watchProperties(obj, propertyNames) { - let watched = {}; - let counters = {}; + const watched = {}; + const counters = {}; if (!Array.isArray(propertyNames)) { throw new Error(`Must call watchProperties with an array of propertyNames to watch, received ${propertyNames}`); } for (let i = 0; i < propertyNames.length; i++) { - let propertyName = propertyNames[i]; + const propertyName = propertyNames[i]; if (watched[propertyName] !== undefined) { throw new Error(`Cannot watch the same property ${propertyName} more than once`); } - let { counter, increment } = makeCounter(); + const { counter, increment } = makeCounter(); watched[propertyName] = increment; counters[propertyName] = counter; } @@ -100,10 +101,10 @@ QUnit.assert.watchedPropertyCounts = function assertWatchedPropertyCount(watched throw new Error('Expected to receive the return value of watchProperties: an object containing counters'); } - let counters = watchedObject.counters; + const counters = watchedObject.counters; Object.keys(expectedCounts).forEach((propertyName) => { - let counter = counters[propertyName]; + const counter = counters[propertyName]; let expectedCount = expectedCounts[propertyName]; let assertionText = label; diff --git a/tests/main/tests/index.html b/tests/main/tests/index.html index a4c2ea4f7ef..2f76a549ebb 100644 --- a/tests/main/tests/index.html +++ b/tests/main/tests/index.html @@ -21,6 +21,23 @@ {{content-for "body"}} {{content-for "test-body"}} + +
          @@ -30,31 +47,37 @@ diff --git a/tests/main/tests/integration/adapter/build-url-mixin-test.js b/tests/main/tests/integration/adapter/build-url-mixin-test.js index 565a1c632cb..205ab750696 100644 --- a/tests/main/tests/integration/adapter/build-url-mixin-test.js +++ b/tests/main/tests/integration/adapter/build-url-mixin-test.js @@ -1,15 +1,11 @@ -import { decamelize, underscore } from '@ember/string'; - import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; -import { pluralize } from 'ember-inflector'; import { setupTest } from 'ember-qunit'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { dasherize, pluralize, underscore } from '@ember-data/request-utils/string'; import RESTSerializer from '@ember-data/serializer/rest'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', function (hooks) { setupTest(hooks); @@ -20,16 +16,16 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f adapter.ajax = function (url, verb, hash) { passedUrl = url; - return resolve(deepCopy(value)); + return Promise.resolve(structuredClone(value)); }; } hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; class SuperUser extends Model {} - owner.register('adapter:application', RESTAdapter.extend()); - owner.register('serializer:application', RESTSerializer.extend()); + owner.register('adapter:application', class extends RESTAdapter {}); + owner.register('serializer:application', class extends RESTSerializer {}); owner.register('model:super-user', SuperUser); store = owner.lookup('service:store'); @@ -82,7 +78,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ posts: [{ id: '1', links: { comments: 'comments' } }] }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [{ id: '1' }] }); await post.comments; @@ -109,7 +105,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ posts: [{ id: '1', links: { comments: '/api/v1/posts/1/comments' } }] }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [{ id: '1' }] }); await post.comments; @@ -136,7 +132,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ posts: [{ id: '1', links: { comments: '/api/v1/posts/1/comments' } }] }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [{ id: '1' }] }); await post.comments; @@ -163,7 +159,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ posts: [{ id: '1', links: { comments: '/api/v1/posts/1/comments' } }] }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [{ id: '1' }] }); await post.comments; @@ -197,7 +193,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ], }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [{ id: '1' }] }); await post.comments; @@ -207,8 +203,8 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f test('buildURL - with camelized names', async function (assert) { adapter.setProperties({ pathForType(type) { - let decamelized = decamelize(type); - return underscore(pluralize(decamelized)); + const dasherized = dasherize(type); + return underscore(pluralize(dasherized)); }, }); @@ -237,7 +233,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ comments: [{ id: '1' }] }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', @@ -271,7 +267,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f adapter.coalesceFindRequests = true; ajaxResponse({ comments: [{ id: '1' }, { id: '2' }, { id: '3' }] }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', @@ -310,13 +306,13 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ comments: [{ id: '1' }] }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', }, }); - let comment = store.createRecord('comment'); + const comment = store.createRecord('comment'); comment.set('post', post); await comment.save(); assert.strictEqual(passedUrl, '/posts/2/comments/'); @@ -339,7 +335,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f return '/posts/' + snapshot.belongsTo('post', { id: true }) + '/comments/'; }; - let post = store.push({ + const post = store.push({ data: { id: '2', type: 'post', @@ -351,7 +347,7 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ comments: [{ id: '1' }] }); - let comment = store.createRecord('comment'); + const comment = store.createRecord('comment'); comment.set('post', post); await comment.save(); @@ -378,13 +374,13 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ comments: [{ id: '1' }] }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', }, }); - let comment = store.push({ + const comment = store.push({ data: { type: 'comment', id: '1', @@ -415,13 +411,13 @@ module('integration/adapter/build-url-mixin - BuildURLMixin with RESTAdapter', f ajaxResponse({ comments: [{ id: '1' }] }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', }, }); - let comment = store.push({ + const comment = store.push({ data: { type: 'comment', id: '1', diff --git a/tests/main/tests/integration/adapter/client-side-delete-test.js b/tests/main/tests/integration/adapter/client-side-delete-test.js index ed43c66889b..becb9ceee78 100644 --- a/tests/main/tests/integration/adapter/client-side-delete-test.js +++ b/tests/main/tests/integration/adapter/client-side-delete-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -27,18 +26,18 @@ module('integration/adapter/store-adapter - client-side delete', function (hooks this.owner.register('model:bookstore', Bookstore); this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (_store, _modelClass, snapshot) { if (snapshot.adapterOptions.clientSideDelete) { - return resolve(); + return Promise.resolve(); } assert.ok(false, 'unreachable'); }; - let bookstore = store.push({ + const bookstore = store.push({ data: { id: '1', type: 'bookstore', @@ -75,7 +74,7 @@ module('integration/adapter/store-adapter - client-side delete', function (hooks 'initial hasmany loaded' ); - let book2 = store.peekRecord('book', '2'); + const book2 = store.peekRecord('book', '2'); await book2.destroyRecord({ adapterOptions: { clientSideDelete: true } }); diff --git a/tests/main/tests/integration/adapter/find-all-test.js b/tests/main/tests/integration/adapter/find-all-test.js index 6674a1606fd..71d413c536c 100644 --- a/tests/main/tests/integration/adapter/find-all-test.js +++ b/tests/main/tests/integration/adapter/find-all-test.js @@ -1,11 +1,11 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { defer, reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; 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'; @@ -28,7 +28,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('serializer:application', class extends JSONAPISerializer {}); @@ -38,13 +38,13 @@ module('integration/adapter/find-all - Finding All Records of a Type', function test("When all records for a type are requested, the store should call the adapter's `findAll` method.", async function (assert) { assert.expect(5); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); adapter.findAll = () => { // this will get called twice assert.ok(true, "the adapter's findAll method should be invoked"); - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -57,11 +57,11 @@ module('integration/adapter/find-all - Finding All Records of a Type', function }); }; - let allRecords = await store.findAll('person'); + const allRecords = await store.findAll('person'); assert.strictEqual(allRecords.length, 1, "the record array's length is 1 after a record is loaded into it"); assert.strictEqual(allRecords[0].name, 'Braaaahm Dale', 'the first item in the record array is Braaaahm Dale'); - let all = await store.findAll('person'); + const all = await store.findAll('person'); // Only one record array per type should ever be created (identity map) assert.strictEqual( allRecords, @@ -72,7 +72,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function test('When all records for a type are requested, a rejection should reject the promise', async function (assert) { assert.expect(5); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); let count = 0; adapter.findAll = () => { @@ -80,9 +80,9 @@ module('integration/adapter/find-all - Finding All Records of a Type', function assert.ok(true, "the adapter's findAll method should be invoked"); if (count++ === 0) { - return reject(); + return Promise.reject(); } else { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -96,7 +96,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function } }; - let all = await store.findAll('person').catch(() => { + const all = await store.findAll('person').catch(() => { assert.ok(true, 'The rejection should get here'); return store.findAll('person'); }); @@ -121,7 +121,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function // Create a new, unsaved record in the store store.createRecord('person', { name: 'Alex MacCaw' }); - let allRecords = store.peekAll('person'); + const allRecords = store.peekAll('person'); assert.strictEqual(allRecords.length, 2, "the record array's length is 2"); assert.strictEqual(allRecords[0].name, 'Jeremy Ashkenas', 'the first item in the record array is Jeremy Ashkenas'); @@ -131,7 +131,7 @@ module('integration/adapter/find-all - Finding All Records of a Type', function test('When all records for a type are requested, records that are created on the client should be added to the record array.', async function (assert) { assert.expect(3); - let allRecords = store.peekAll('person'); + const allRecords = store.peekAll('person'); assert.strictEqual( allRecords.length, @@ -147,8 +147,8 @@ module('integration/adapter/find-all - Finding All Records of a Type', function }); testInDebug('When all records are requested, assert the payload is not blank', async function (assert) { - let adapter = store.adapterFor('person'); - adapter.findAll = () => resolve({}); + const adapter = store.adapterFor('person'); + adapter.findAll = () => Promise.resolve({}); assert.expectAssertion( () => store.findAll('person'), @@ -157,8 +157,8 @@ module('integration/adapter/find-all - Finding All Records of a Type', function }); test('isUpdating is true while records are fetched', async function (assert) { - let findAllDeferred = defer(); - let adapter = store.adapterFor('person'); + const findAllDeferred = createDeferred(); + const adapter = store.adapterFor('person'); adapter.findAll = () => findAllDeferred.promise; adapter.shouldReloadAll = () => true; @@ -171,10 +171,10 @@ module('integration/adapter/find-all - Finding All Records of a Type', function ], }); - let persons = store.peekAll('person'); + const persons = store.peekAll('person'); assert.strictEqual(persons.length, 1); - let promise = store.findAll('person'); + const promise = store.findAll('person'); assert.true(persons.isUpdating); @@ -186,8 +186,8 @@ module('integration/adapter/find-all - Finding All Records of a Type', function }); test('isUpdating is true while records are fetched in the background', async function (assert) { - let findAllDeferred = defer(); - let adapter = store.adapterFor('person'); + const findAllDeferred = createDeferred(); + const adapter = store.adapterFor('person'); adapter.findAll = () => { return findAllDeferred.promise; }; @@ -223,8 +223,8 @@ module('integration/adapter/find-all - Finding All Records of a Type', function }); test('isUpdating is false if records are not fetched in the background', async function (assert) { - let findAllDeferred = defer(); - let adapter = store.adapterFor('person'); + const findAllDeferred = createDeferred(); + const adapter = store.adapterFor('person'); adapter.findAll = () => { return findAllDeferred.promise; }; diff --git a/tests/main/tests/integration/adapter/find-test.js b/tests/main/tests/integration/adapter/find-test.js index 2846e28752c..30cdd8774e9 100644 --- a/tests/main/tests/integration/adapter/find-test.js +++ b/tests/main/tests/integration/adapter/find-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { all, allSettled, Promise, reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -8,29 +7,9 @@ import Model, { attr } 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'; -module('integration/adapter/find - Finding Records', function (hooks) { +module('integration/adapter - Finding Records', function (hooks) { setupTest(hooks); - testInDebug('It raises an assertion when `undefined` is passed as id (#1705)', async function (assert) { - class Person extends Model { - @attr('string') name; - } - - this.owner.register('model:person', Person); - this.owner.register('adapter:application', Adapter.extend()); - this.owner.register('serializer:application', class extends JSONAPISerializer {}); - - const store = this.owner.lookup('service:store'); - - await assert.expectAssertion(async () => { - await store.find('person', undefined); - }, `You cannot pass 'undefined' as id to the store's find method`); - - await assert.expectAssertion(async () => { - await store.find('person', null); - }, `You cannot pass 'null' as id to the store's find method`); - }); - test("When a single record is requested, the adapter's find method should be called unless it's loaded.", async function (assert) { assert.expect(2); @@ -68,8 +47,8 @@ module('integration/adapter/find - Finding Records', function (hooks) { const store = this.owner.lookup('service:store'); - let promise1 = store.findRecord('person', '1'); - let promise2 = store.findRecord('person', '1'); + const promise1 = store.findRecord('person', '1'); + const promise2 = store.findRecord('person', '1'); await promise1; await promise2; @@ -85,7 +64,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { this.owner.register('serializer:application', class extends JSONAPISerializer {}); let resolveFindRecordPromise; - let findRecordPromise = new Promise((resolve) => (resolveFindRecordPromise = resolve)); + const findRecordPromise = new Promise((resolve) => (resolveFindRecordPromise = resolve)); this.owner.register( 'adapter:person', @@ -96,14 +75,14 @@ module('integration/adapter/find - Finding Records', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let firstPlayerRequest = store.findRecord('person', '1').then(function (firstPlayerRequest) { + const firstPlayerRequest = store.findRecord('person', '1').then(function (firstPlayerRequest) { assert.strictEqual(firstPlayerRequest.id, '1'); assert.strictEqual(firstPlayerRequest.name, 'Totono Grisales'); }); - let secondPlayerRequest = store.findRecord('person', '1').then(function (secondPlayerRequest) { + const secondPlayerRequest = store.findRecord('person', '1').then(function (secondPlayerRequest) { assert.strictEqual(secondPlayerRequest.id, '1'); assert.strictEqual(secondPlayerRequest.name, 'Totono Grisales'); }); @@ -118,7 +97,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { }, }); - await allSettled([firstPlayerRequest, secondPlayerRequest]); + await Promise.allSettled([firstPlayerRequest, secondPlayerRequest]); }); test('When a single record is requested, and the promise is rejected, .findRecord() is rejected.', async function (assert) { @@ -133,7 +112,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return reject(); + return Promise.reject(); }, }) ); @@ -143,7 +122,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { try { await store.findRecord('person', '1'); assert.ok(false, 'We expected to throw but did not'); - } catch (e) { + } catch { assert.ok(true, 'The rejection handler was called'); } }); @@ -160,7 +139,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return reject(); + return Promise.reject(); }, }) ); @@ -170,7 +149,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { try { await store.findRecord('person', '1'); assert.ok(false, 'We expected to throw but did not'); - } catch (e) { + } catch { assert.ok(true, 'The rejection handler was called'); assert.strictEqual(store.peekRecord('person', '1'), null, 'The record has been unloaded'); } @@ -187,7 +166,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { this.owner.register( 'adapter:person', Adapter.extend({ - findRecord: () => resolve({}), + findRecord: () => Promise.resolve({}), }) ); @@ -198,7 +177,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { assert.ok(false, 'We expected to throw but did not'); } catch (e) { const expectedMessageRegex = - "Assertion Failed: You made a 'findRecord' request for a 'person' with id 'the-id', but the adapter's response did not have any data"; + "You made a 'findRecord' request for a 'person' with id 'the-id', but the adapter's response did not have any data"; assert.strictEqual(expectedMessageRegex, e.message, 'error has the correct error message'); } @@ -216,7 +195,7 @@ module('integration/adapter/find - Finding Records', function (hooks) { 'adapter:person', Adapter.extend({ coalesceFindRequests: true, - findMany: () => resolve({}), + findMany: () => Promise.resolve({}), }) ); @@ -224,10 +203,10 @@ module('integration/adapter/find - Finding Records', function (hooks) { const promises = [store.findRecord('person', '1'), store.findRecord('person', '2')]; try { - await all(promises); + await Promise.all(promises); } catch (e) { const expectedMessageRegex = - "Assertion Failed: You made a 'findMany' request for 'person' records with ids '[1,2]', but the adapter's response did not have any data"; + "You made a 'findMany' request for 'person' records with ids '[1,2]', but the adapter's response did not have any data"; assert.strictEqual(expectedMessageRegex, e.message, 'error has the correct error message'); } diff --git a/tests/main/tests/integration/adapter/handle-response-test.js b/tests/main/tests/integration/adapter/handle-response-test.js index c08b60f2ae1..1627bef5bdb 100644 --- a/tests/main/tests/integration/adapter/handle-response-test.js +++ b/tests/main/tests/integration/adapter/handle-response-test.js @@ -31,7 +31,7 @@ module('integration/adapter/handle-response', function (hooks) { test('handleResponse is called with normal response', async function (assert) { let handleResponseCalled = 0; - let samplePayload = { + const samplePayload = { data: [ { id: '1', @@ -72,7 +72,7 @@ module('integration/adapter/handle-response', function (hooks) { test('handleResponse is called with empty array response', async function (assert) { let handleResponseCalled = 0; - let samplePayload = { + const samplePayload = { data: [], }; @@ -179,7 +179,7 @@ module('integration/adapter/handle-response', function (hooks) { test('handleResponse is called with correct parameters on string response with 422 status', async function (assert) { let handleResponseCalled = 0; - let errorObject = { errors: {} }; + const errorObject = { errors: {} }; this.server.get('/people', function () { return ['422', { 'Content-Type': 'application/json' }, JSON.stringify(errorObject)]; diff --git a/tests/main/tests/integration/adapter/json-api-adapter-test.js b/tests/main/tests/integration/adapter/json-api-adapter-test.js index 6ff354b1eb5..a8790bec90d 100644 --- a/tests/main/tests/integration/adapter/json-api-adapter-test.js +++ b/tests/main/tests/integration/adapter/json-api-adapter-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -96,7 +95,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) passedVerb[index] = verb; passedHash[index] = hash; - return resolve(responses[index]); + return Promise.resolve(responses[index]); }; } @@ -115,7 +114,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'Builds URL correctly'); assert.strictEqual(post.id, '1', 'Stores record with correct id'); @@ -186,7 +185,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let posts = await store.findAll('post'); + const posts = await store.findAll('post'); assert.strictEqual(passedUrl[0], '/posts'); @@ -234,7 +233,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let posts = await store.query('post', { filter: { id: '1' } }); + const posts = await store.query('post', { filter: { id: '1' } }); assert.strictEqual(passedUrl[0], '/posts', 'Builds correct URL'); assert.deepEqual(passedHash[0], { data: { filter: { id: '1' } } }, 'Sends correct params to adapter'); @@ -256,7 +255,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.queryRecord('post', {}); + const post = await store.queryRecord('post', {}); assert.strictEqual(passedUrl[0], '/posts', 'Builds correc URL'); assert.strictEqual(post.title, 'Ember.js rocks', 'Sets correct title to record'); @@ -269,7 +268,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.queryRecord('post', {}); + const post = await store.queryRecord('post', {}); assert.strictEqual(passedUrl[0], '/posts', 'Builds correct URL'); assert.strictEqual(post, null, 'Returns null when adapter response is null'); @@ -324,14 +323,14 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'The primary record post:1 was fetched by the correct url'); assert.strictEqual(post.id, '1', 'Stores record using the correct id'); assert.strictEqual(post.title, 'Ember.js rocks', 'Sets correct title to record'); - let author = await post.author; + const author = await post.author; assert.strictEqual( passedUrl[1], @@ -374,14 +373,14 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'The primary record post:1 was fetched by the correct url'); assert.strictEqual(post.id, '1', 'Stores record using the correct id'); assert.strictEqual(post.title, 'Ember.js rocks', 'Sets correct title to record'); - let author = await post.author; + const author = await post.author; assert.strictEqual(passedUrl[1], '/users/2', 'The relationship user:2 was fetched by the correct url'); assert.strictEqual(author.id, '2', 'Record has correct id'); @@ -420,7 +419,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let user = await store.findRecord('user', '1'); + const user = await store.findRecord('user', '1'); assert.strictEqual(passedUrl[0], '/users/1', 'The primary record user:1 was fetched by the correct url'); @@ -428,7 +427,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(user.firstName, 'Yehuda', 'Sets correct firstName to record'); assert.strictEqual(user.lastName, 'Katz', 'Sets correct lastName to record'); - let company = await user.company; + const company = await user.company; assert.strictEqual( passedUrl[1], @@ -471,14 +470,14 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'The primary record post:1 was fetched by the correct url'); assert.strictEqual(post.id, '1', 'Record has correct id'); assert.strictEqual(post.title, 'Ember.js rocks', 'Title is set correctly'); - let author = await post.author; + const author = await post.author; assert.strictEqual(passedUrl.length, 1); @@ -527,13 +526,13 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1'); assert.strictEqual(post.id, '1'); assert.strictEqual(post.title, 'Ember.js rocks'); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual( passedUrl[1], @@ -586,13 +585,13 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'The primary record post:1 was fetched by the correct url'); assert.strictEqual(post.id, '1', 'Record id is correct'); assert.strictEqual(post.title, 'Ember.js rocks', 'Record title is correct'); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(passedUrl[1], '/comments/2', 'Builds correct URL to fetch related record'); assert.strictEqual(passedUrl[2], '/comments/3', 'Builds correct URL to fetch related record'); @@ -643,7 +642,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let user = await store.findRecord('user', '1'); + const user = await store.findRecord('user', '1'); assert.strictEqual(passedUrl[0], '/users/1', 'The primary record users:1 was fetched by the correct url'); @@ -651,7 +650,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(user.firstName, 'Yehuda', 'Record firstName is loaded'); assert.strictEqual(user.lastName, 'Katz', 'Record lastName is loaded'); - let handles = await user.handles; + const handles = await user.handles; assert.strictEqual(passedUrl[1], '/github-handles/2', 'Builds correct URL to fetch related record'); assert.strictEqual(passedUrl[2], '/twitter-handles/3', 'Builds correct URL to fetch related record'); @@ -700,13 +699,13 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1', 'The primary record post:1 was fetched by the correct url'); assert.strictEqual(post.id, '1', 'Record id is loaded'); assert.strictEqual(post.title, 'Ember.js rocks', 'Record title is loaded'); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(passedUrl.length, 1, 'Do not call extra end points because related records are included'); @@ -755,7 +754,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let user = await store.findRecord('user', '1'); + const user = await store.findRecord('user', '1'); assert.strictEqual(passedUrl[0], '/users/1'); @@ -763,7 +762,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) assert.strictEqual(user.firstName, 'Yehuda'); assert.strictEqual(user.lastName, 'Katz'); - let handles = await user.handles; + const handles = await user.handles; assert.strictEqual(passedUrl.length, 1, 'Do not call extra end points because related records are included'); @@ -792,7 +791,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let company = store.push({ + const company = store.push({ data: { type: 'company', id: '1', @@ -802,7 +801,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let githubHandle = store.push({ + const githubHandle = store.push({ data: { type: 'github-handle', id: '2', @@ -811,7 +810,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }, }); - let twitterHandle = store.push({ + const twitterHandle = store.push({ data: { type: 'twitter-handle', id: '2', @@ -821,13 +820,13 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let user = store.createRecord('user', { + const user = store.createRecord('user', { firstName: 'Yehuda', lastName: 'Katz', company: company, }); - let handles = await user.handles; + const handles = await user.handles; handles.push(githubHandle); handles.push(twitterHandle); @@ -883,7 +882,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -894,7 +893,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let company = store.push({ + const company = store.push({ data: { type: 'company', id: '2', @@ -904,7 +903,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let githubHandle = store.push({ + const githubHandle = store.push({ data: { type: 'github-handle', id: '3', @@ -917,7 +916,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) user.set('firstName', 'Yehuda!'); user.set('company', company); - let handles = await user.handles; + const handles = await user.handles; handles.push(githubHandle); @@ -969,7 +968,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) } ); - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -980,7 +979,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let githubHandle = store.push({ + const githubHandle = store.push({ data: { type: 'github-handle', id: '2', @@ -990,7 +989,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, }); - let twitterHandle = store.push({ + const twitterHandle = store.push({ data: { type: 'twitter-handle', id: '3', @@ -1002,7 +1001,7 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) user.set('firstName', 'Yehuda!'); - let handles = await user.handles; + const handles = await user.handles; handles.push(githubHandle); handles.push(twitterHandle); @@ -1059,11 +1058,11 @@ module('integration/adapter/json-api-adapter - JSONAPIAdapter', function (hooks) }, ]); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); assert.strictEqual(passedUrl[0], '/posts/1'); - let author = await post.author; + const author = await post.author; assert.strictEqual(passedUrl[1], 'http://example.com/post/1/author'); assert.strictEqual(author, null); diff --git a/tests/main/tests/integration/adapter/queries-test.js b/tests/main/tests/integration/adapter/queries-test.js index 7a332cbbecc..e460bab87e4 100644 --- a/tests/main/tests/integration/adapter/queries-test.js +++ b/tests/main/tests/integration/adapter/queries-test.js @@ -1,7 +1,6 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise as EmberPromise, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -23,7 +22,7 @@ module('integration/adapter/queries - Queries', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { store.query(); @@ -35,7 +34,7 @@ module('integration/adapter/queries - Queries', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { store.query('person'); @@ -49,13 +48,13 @@ module('integration/adapter/queries - Queries', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function (store, type, query, recordArray) { assert.strictEqual(type, Person, 'the query method is called with the correct type'); - return EmberPromise.resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -75,7 +74,7 @@ module('integration/adapter/queries - Queries', function (hooks) { }); }; - let queryResults = await store.query('person', { page: '1' }); + const queryResults = await store.query('person', { page: '1' }); assert.strictEqual(queryResults.length, 2, 'the record array has a length of 2 after the results are loaded'); assert.true(queryResults.isLoaded, "the record array's `isLoaded` property should be true"); @@ -91,14 +90,14 @@ module('integration/adapter/queries - Queries', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function () { - return resolve({ data: [{ id: 'first', type: 'person' }] }); + return Promise.resolve({ data: [{ id: 'first', type: 'person' }] }); }; - let personsQuery = await store.query('person', {}); + const personsQuery = await store.query('person', {}); assert.strictEqual(personsQuery.length, 1, 'There is one person'); assert.strictEqual(personsQuery.at(0).id, 'first', 'the right person is present'); @@ -140,13 +139,13 @@ module('integration/adapter/queries - Queries', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function (store, type, query, recordArray) { assert.strictEqual(type, Person, 'the query method is called with the correct type'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Peter Wagenet' } }, }); }; diff --git a/tests/main/tests/integration/adapter/record-persistence-test.js b/tests/main/tests/integration/adapter/record-persistence-test.js index 96e4bb5c4b1..faa96d35110 100644 --- a/tests/main/tests/integration/adapter/record-persistence-test.js +++ b/tests/main/tests/integration/adapter/record-persistence-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { allSettled, hash, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -28,14 +27,14 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (_store, type, snapshot) { assert.strictEqual(type, Person, "The type of the record is 'Person'"); assert.strictEqual(snapshot.record, tom, 'The record in the snapshot is the correct one'); - return resolve(); + return Promise.resolve(); }; const tom = store.push({ @@ -72,16 +71,17 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + // eslint-disable-next-line prefer-const let tom; adapter.createRecord = function (_store, type, snapshot) { assert.strictEqual(type, Person, "The type of the record is 'Person'"); assert.strictEqual(snapshot.record, tom, 'The record in the snapshot is the correct one'); - return resolve({ data: { id: '1', type: 'person', attributes: { name: 'Tom Dale' } } }); + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Tom Dale' } } }); }; tom = store.createRecord('person', { name: 'Tom Dale' }); @@ -108,19 +108,19 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let tom; adapter.createRecord = function (store, type, snapshot) { - return resolve({ data: { id: '1', type: 'person', attributes: { name: 'Tom Dale' } } }); + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Tom Dale' } } }); }; tom = store.createRecord('person', { name: 'Tom Dale' }); tom = await tom.save(); - let retrievedTom = await store.findRecord('person', '1'); + const retrievedTom = await store.findRecord('person', '1'); assert.strictEqual(tom, retrievedTom, 'The retrieved record is the same as the created record'); }); @@ -142,14 +142,14 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (_store, type, snapshot) { assert.strictEqual(type, Person, "The type of the record is 'Person'"); assert.strictEqual(snapshot.record, tom, 'The record in the snapshot is the correct one'); - return resolve(); + return Promise.resolve(); }; store.push({ @@ -162,7 +162,7 @@ module('integration/adapter/record_persistence - Persisting Records', function ( }, }); - let tom = await store.findRecord('person', '1'); + const tom = await store.findRecord('person', '1'); tom.deleteRecord(); @@ -188,12 +188,12 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (_store, _type, snapshot) { if (snapshot.id === '1') { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -205,7 +205,7 @@ module('integration/adapter/record_persistence - Persisting Records', function ( }); } - return resolve({ + return Promise.resolve({ data: { id: '2', type: 'person', @@ -233,10 +233,7 @@ module('integration/adapter/record_persistence - Persisting Records', function ( ], }); - let { tom, yehuda } = await hash({ - tom: store.findRecord('person', '1'), - yehuda: store.findRecord('person', '2'), - }); + const [tom, yehuda] = await Promise.all([store.findRecord('person', '1'), store.findRecord('person', '2')]); tom.set('name', 'Draaaaaahm Dale'); yehuda.set('name', 'Goy Katz'); @@ -244,12 +241,12 @@ module('integration/adapter/record_persistence - Persisting Records', function ( assert.true(tom.hasDirtyAttributes, 'Tom is dirty'); assert.true(yehuda.hasDirtyAttributes, 'Yehuda is dirty'); - let [{ value: savedTom }, { value: savedYehuda }] = await allSettled([tom.save(), yehuda.save()]); + const [{ value: savedTom }, { value: savedYehuda }] = await Promise.allSettled([tom.save(), yehuda.save()]); assert.strictEqual(savedTom, tom, 'The record is correct'); assert.strictEqual(savedYehuda, yehuda, 'The record is correct'); assert.false(tom.hasDirtyAttributes, 'Tom is not dirty after saving record'); - assert.false(yehuda.hasDirtyAttributes, 'Yehuda is not dirty after dsaving record'); + assert.false(yehuda.hasDirtyAttributes, 'Yehuda is not dirty after saving record'); assert.strictEqual(tom.name, 'Tom Dale', 'name attribute should reflect value of hash returned from the request'); assert.strictEqual( tom.updatedAt, @@ -285,10 +282,10 @@ module('integration/adapter/record_persistence - Persisting Records', function ( this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - adapter.deleteRecord = () => resolve(); + adapter.deleteRecord = () => Promise.resolve(); store.push({ data: [ @@ -309,16 +306,13 @@ module('integration/adapter/record_persistence - Persisting Records', function ( ], }); - let { tom, yehuda } = await hash({ - tom: store.findRecord('person', '1'), - yehuda: store.findRecord('person', '2'), - }); + const [tom, yehuda] = await Promise.all([store.findRecord('person', '1'), store.findRecord('person', '2')]); assert.false(tom.isDeleted, 'Tom is not deleted'); assert.false(yehuda.isDeleted, 'Yehuda is not deleted'); - await allSettled([tom.deleteRecord(), yehuda.deleteRecord()]); - await allSettled([tom.save(), yehuda.save()]); + await Promise.allSettled([tom.deleteRecord(), yehuda.deleteRecord()]); + await Promise.allSettled([tom.save(), yehuda.save()]); assert.true(tom.isDeleted, 'Tom is marked as deleted'); assert.true(yehuda.isDeleted, 'Yehuda is marked as deleted'); @@ -350,16 +344,17 @@ module('integration/adapter/record_persistence - Persisting Records', function ( }) ); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + // eslint-disable-next-line prefer-const let tom; adapter.createRecord = function (_store, type, snapshot) { assert.strictEqual(type, Person, "The type of the record is 'Person'"); assert.strictEqual(snapshot.record, tom, 'The record in the snapshot is the correct one'); - return resolve({ data: { id: '1' } }); + return Promise.resolve({ data: { id: '1' } }); }; tom = store.createRecord('person', { name: 'Tom Dale' }); diff --git a/tests/main/tests/integration/adapter/rest-adapter-test.js b/tests/main/tests/integration/adapter/rest-adapter-test.js index 1cb07ef40af..aaff89f28be 100644 --- a/tests/main/tests/integration/adapter/rest-adapter-test.js +++ b/tests/main/tests/integration/adapter/rest-adapter-test.js @@ -1,11 +1,8 @@ import { get } from '@ember/object'; -import { underscore } from '@ember/string'; import Pretender from 'pretender'; import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; -import { singularize } from 'ember-inflector'; import { setupTest } from 'ember-qunit'; import AdapterError, { @@ -19,16 +16,22 @@ import AdapterError, { import RESTAdapter from '@ember-data/adapter/rest'; import { Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { singularize, underscore } from '@ember-data/request-utils/string'; import RESTSerializer from '@ember-data/serializer/rest'; import { recordIdentifierFor } from '@ember-data/store'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; +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_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; let store, adapter, SuperUser; let passedUrl, passedVerb, passedHash; let server; +function isSnapshot(snapshot) { + return snapshot instanceof Snapshot || snapshot.constructor.name === 'Snapshot'; +} + module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { setupTest(hooks); @@ -59,9 +62,9 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { passedHash = hash; passedUrl = passedHash.url; passedVerb = passedHash.method; - return resolve({ + return Promise.resolve({ text() { - return resolve(JSON.stringify(deepCopy(value))); + return Promise.resolve(JSON.stringify(structuredClone(value))); }, ok: true, status: 200, @@ -73,15 +76,15 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { passedVerb = verb; passedHash = hash; - return resolve(deepCopy(value)); + return Promise.resolve(structuredClone(value)); }; } function ajaxError(responseText, status = 400, headers = {}) { adapter._fetchRequest = () => { - return resolve({ + return Promise.resolve({ text() { - return resolve(responseText); + return Promise.resolve(responseText); }, ok: false, status, @@ -90,12 +93,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }; adapter._ajaxRequest = (hash) => { - let jqXHR = { + const jqXHR = { status, responseText, getAllResponseHeaders() { - let reducer = (prev, key) => prev + key + ': ' + headers[key] + '\r\n'; - let stringify = (headers) => { + const reducer = (prev, key) => prev + key + ': ' + headers[key] + '\r\n'; + const stringify = (headers) => { return Object.keys(headers).reduce(reducer, ''); }; return stringify(headers); @@ -107,9 +110,9 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { function ajaxZero() { adapter._fetchRequest = () => { - return resolve({ + return Promise.resolve({ text() { - return resolve(); + return Promise.resolve(); }, ok: false, status: 0, @@ -147,7 +150,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); ajaxResponse(); post.set('name', 'The Parley Letter'); @@ -186,7 +189,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse(); post.set('name', 'The Parley Letter'); @@ -216,7 +219,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ posts: [{ id: '1', name: 'Dat Parley Letter' }] }); post.set('name', 'The Parley Letter'); @@ -251,7 +254,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ post: { id: '1', name: 'Dat Parley Letter' } }); post.set('name', 'The Parley Letter'); @@ -275,13 +278,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { } this.owner.register('model:post', Post); - let post; ajaxResponse({ posts: [{ id: '1', name: 'Dat Parley Letter' }], comments: [{ id: '1', name: 'FIRST' }], }); - post = store.createRecord('post', { name: 'The Parley Letter' }); + const post = store.createRecord('post', { name: 'The Parley Letter' }); await post.save(); assert.strictEqual(passedUrl, '/posts', 'we pass the correct url'); assert.strictEqual(passedVerb, 'POST', 'we pass the correct http method'); @@ -291,11 +293,11 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.false(post.hasDirtyAttributes, "the post isn't dirty anymore"); assert.strictEqual(post.name, 'Dat Parley Letter', 'the post was updated'); - let comment = store.peekRecord('comment', '1'); + const comment = store.peekRecord('comment', '1'); assert.strictEqual(comment.name, 'FIRST', 'The comment was sideloaded'); }); - test('updateRecord - a payload with sideloaded updates pushes the updates', async function (assert) { + test('updateRecord - a payload with sideloaded updates pushes the updates, v2', async function (assert) { class Comment extends Model { @attr name; } @@ -317,7 +319,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ posts: [{ id: '1', name: 'Dat Parley Letter' }], comments: [{ id: '1', name: 'FIRST' }], @@ -332,7 +334,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.false(post.hasDirtyAttributes, "the post isn't dirty anymore"); assert.strictEqual(post.name, 'Dat Parley Letter', 'the post was updated'); - let comment = store.peekRecord('comment', 1); + const comment = store.peekRecord('comment', 1); assert.strictEqual(comment.name, 'FIRST', 'The comment was sideloaded'); }); @@ -369,7 +371,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxResponse(); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); post.set('name', 'The Parley Letter'); await post.save(); @@ -426,10 +428,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: { id: '1', name: 'Not everyone uses Rails', comments: [2] }, }); - await store.findRecord('comment', 2); - let post = await store.findRecord('post', 1); - let newComment = store.peekRecord('comment', 2); - let comments = post.comments; + await store.findRecord('comment', '2'); + const post = await store.findRecord('post', '1'); + const newComment = store.peekRecord('comment', '2'); + const comments = post.comments; // Replace the comment with a new one comments.pop(); @@ -490,7 +492,63 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.strictEqual(post.comments.length, 0, 'the post has the no comments'); }); - test('updateRecord - hasMany relationships set locally will be removed with empty response', async function (assert) { + deprecatedTest( + 'updateRecord - hasMany relationships set locally will be removed with empty response', + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + until: '6.0', + count: 1, + }, + async function (assert) { + class Post extends Model { + @attr name; + @hasMany('comment', { async: false, inverse: 'post' }) comments; + } + this.owner.register('model:post', Post); + class Comment extends Model { + @attr name; + @belongsTo('post', { async: false, inverse: 'comments' }) post; + } + + this.owner.register('model:comment', Comment); + + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + name: 'Not everyone uses Rails', + }, + }, + }); + + store.push({ + data: { + type: 'comment', + id: '1', + attributes: { + name: 'Rails is omakase', + }, + }, + }); + + ajaxResponse({ + posts: { id: '1', name: 'Everyone uses Rails', comments: [] }, + }); + + let post = await store.peekRecord('post', 1); + const comment = await store.peekRecord('comment', 1); + const comments = post.comments; + comments.push(comment); + assert.strictEqual(post.comments.length, 1, 'the post has one comment'); + + post = await post.save(); + + assert.strictEqual(post.comments.length, 0, 'the post has the no comments'); + } + ); + + test('updateRecord - hasMany relationships set locally will NOT be removed with empty response when flag is set', async function (assert) { class Post extends Model { @attr name; @hasMany('comment', { async: false, inverse: 'post' }) comments; @@ -498,17 +556,22 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { this.owner.register('model:post', Post); class Comment extends Model { @attr name; - @belongsTo('post', { async: false, inverse: 'comments' }) post; + @belongsTo('post', { async: false, inverse: 'comments', resetOnRemoteUpdate: false }) post; } this.owner.register('model:comment', Comment); - store.push({ + const post = store.push({ data: { type: 'post', id: '1', attributes: { - name: 'Not everyone uses Rails', + name: 'Not everyone uses React', + }, + relationships: { + comments: { + data: [{ type: 'comment', id: '1' }], + }, }, }, }); @@ -518,7 +581,17 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { type: 'comment', id: '1', attributes: { - name: 'Rails is omakase', + name: 'Go is omakase', + }, + }, + }); + + const comment2 = store.push({ + data: { + type: 'comment', + id: '2', + attributes: { + name: 'Ember is omakase', }, }, }); @@ -527,17 +600,78 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: { id: '1', name: 'Everyone uses Rails', comments: [] }, }); - let post = await store.peekRecord('post', 1); - let comment = await store.peekRecord('comment', 1); - let comments = post.comments; - comments.push(comment); - assert.strictEqual(post.comments.length, 1, 'the post has one comment'); + post.comments.push(comment2); + assert.strictEqual(post.comments.length, 2, 'the post has two comments'); - post = await post.save(); + await post.save(); - assert.strictEqual(post.comments.length, 0, 'the post has the no comments'); + assert.strictEqual(post.comments.length, 1, 'the post has one comment'); + assert.strictEqual(post.comments.at(0).id, '2', 'the post has the correct comment'); }); + if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + test('updateRecord - hasMany relationships set locally will NOT be removed with empty response', async function (assert) { + class Post extends Model { + @attr name; + @hasMany('comment', { async: false, inverse: 'post' }) comments; + } + this.owner.register('model:post', Post); + class Comment extends Model { + @attr name; + @belongsTo('post', { async: false, inverse: 'comments' }) post; + } + + this.owner.register('model:comment', Comment); + + const post = store.push({ + data: { + type: 'post', + id: '1', + attributes: { + name: 'Not everyone uses React', + }, + relationships: { + comments: { + data: [{ type: 'comment', id: '1' }], + }, + }, + }, + }); + + store.push({ + data: { + type: 'comment', + id: '1', + attributes: { + name: 'Go is omakase', + }, + }, + }); + + const comment2 = store.push({ + data: { + type: 'comment', + id: '2', + attributes: { + name: 'Ember is omakase', + }, + }, + }); + + ajaxResponse({ + posts: { id: '1', name: 'Everyone uses Rails', comments: [] }, + }); + + post.comments.push(comment2); + assert.strictEqual(post.comments.length, 2, 'the post has two comments'); + + await post.save(); + + assert.strictEqual(post.comments.length, 1, 'the post has one comment'); + assert.strictEqual(post.comments.at(0).id, '2', 'the post has the correct comment'); + }); + } + test('deleteRecord - an empty payload is a basic success', async function (assert) { class Post extends Model { @attr name; @@ -561,7 +695,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse(); post.deleteRecord(); @@ -602,7 +736,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse(); post.deleteRecord(); @@ -634,7 +768,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ comments: [{ id: '1', name: 'FIRST' }] }); post.deleteRecord(); @@ -647,7 +781,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.false(post.hasDirtyAttributes, "the post isn't dirty anymore"); assert.true(post.isDeleted, 'the post is now deleted'); - let comment = store.peekRecord('comment', 1); + const comment = store.peekRecord('comment', 1); assert.strictEqual(comment.name, 'FIRST', 'The comment was sideloaded'); }); @@ -674,7 +808,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ posts: [{ id: '2', name: 'The Parley Letter' }] }); post.deleteRecord(); @@ -687,7 +821,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.false(post.hasDirtyAttributes, "the original post isn't dirty anymore"); assert.true(post.isDeleted, 'the original post is now deleted'); - let newPost = store.peekRecord('post', 2); + const newPost = store.peekRecord('post', 2); assert.strictEqual(newPost.name, 'The Parley Letter', 'The new post was added to the store'); }); @@ -703,8 +837,8 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { } this.owner.register('model:comment', Comment); - let post = store.createRecord('post'); - let identifier = recordIdentifierFor(post); + const post = store.createRecord('post'); + const identifier = recordIdentifierFor(post); post.deleteRecord(); await post.save(); @@ -738,20 +872,20 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let posts = await store.findAll('post'); + const posts = await store.findAll('post'); assert.strictEqual(passedUrl, '/posts'); assert.strictEqual(passedVerb, 'GET'); assert.deepEqual(passedHash.data, {}); - let post1 = store.peekRecord('post', 1); - let post2 = store.peekRecord('post', 2); + const post1 = store.peekRecord('post', 1); + const post2 = store.peekRecord('post', 2); assert.deepEqual(post1.getProperties('id', 'name'), { id: '1', name: 'Rails is omakase' }, 'Post 1 is loaded'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }, 'Post 2 is loaded'); assert.strictEqual(posts.length, 2, 'The posts are in the array'); - assert.true(posts.isLoaded, 'The RecordArray is loaded'); + assert.true(posts.isLoaded, 'The LiveArray is loaded'); assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); @@ -768,7 +902,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { this.owner.register('model:comment', Comment); assert.expect(2); - let adapterOptionsStub = { stub: true }; + const adapterOptionsStub = { stub: true }; adapter.buildURL = function (type, id, snapshot, requestType) { assert.strictEqual(snapshot.adapterOptions, adapterOptionsStub); return '/' + requestType + '/posts'; @@ -827,7 +961,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { await store.findAll('post'); - let comment = store.peekRecord('comment', '1'); + const comment = store.peekRecord('comment', '1'); assert.deepEqual(comment.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); }); @@ -858,15 +992,15 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let posts = await store.findAll('post'); - let post1 = store.peekRecord('post', 1); - let post2 = store.peekRecord('post', 2); + const posts = await store.findAll('post'); + const post1 = store.peekRecord('post', 1); + const post2 = store.peekRecord('post', 2); assert.deepEqual(post1.getProperties('id', 'name'), { id: '1', name: 'Rails is omakase' }, 'Post 1 is loaded'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }, 'Post 2 is loaded'); assert.strictEqual(posts.length, 2, 'The posts are in the array'); - assert.true(posts.isLoaded, 'The RecordArray is loaded'); + assert.true(posts.isLoaded, 'The LiveArray is loaded'); assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); @@ -961,11 +1095,11 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }); adapter.sortQueryParams = function (obj) { - let sortedKeys = Object.keys(obj).sort().reverse(); - let len = sortedKeys.length; - let newQueryParams = {}; + const sortedKeys = Object.keys(obj).sort().reverse(); + const len = sortedKeys.length; + const newQueryParams = {}; - for (var i = 0; i < len; i++) { + for (let i = 0; i < len; i++) { newQueryParams[sortedKeys[i]] = obj[sortedKeys[i]]; } return newQueryParams; @@ -996,7 +1130,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: [{ id: '1', name: 'Rails is very expensive sushi' }], }); - let posts = await store.query('post', { page: 2 }); + const posts = await store.query('post', { page: 2 }); assert.strictEqual(posts.meta.offset, 5, 'Reponse metadata can be accessed with recordArray.meta'); }); @@ -1017,14 +1151,14 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: [{ id: '1', name: 'Rails is very expensive sushi' }], }); - let posts = await store.query('post', { page: 2 }); + const posts = await store.query('post', { page: 2 }); assert.strictEqual(posts.meta.offset, 5, 'Reponse metadata can be accessed with recordArray.meta'); ajaxResponse({ meta: { offset: 1 }, posts: [{ id: '1', name: 'Rails is very expensive sushi' }], }); - let newPosts = await store.query('post', { page: 1 }); + const newPosts = await store.query('post', { page: 1 }); assert.strictEqual(newPosts.meta.offset, 1, 'new array has correct metadata'); assert.strictEqual(posts.meta.offset, 5, 'metadata on the old array hasnt been clobbered'); }); @@ -1048,19 +1182,19 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let posts = await store.query('post', { page: 1 }); + const posts = await store.query('post', { page: 1 }); assert.strictEqual(passedUrl, '/posts'); assert.strictEqual(passedVerb, 'GET'); assert.deepEqual(passedHash.data, { page: 1 }); - let post1 = store.peekRecord('post', 1); - let post2 = store.peekRecord('post', 2); + const post1 = store.peekRecord('post', 1); + const post2 = store.peekRecord('post', 2); assert.deepEqual(post1.getProperties('id', 'name'), { id: '1', name: 'Rails is omakase' }, 'Post 1 is loaded'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }, 'Post 2 is loaded'); assert.strictEqual(posts.length, 2, 'The posts are in the array'); - assert.true(posts.isLoaded, 'The RecordArray is loaded'); + assert.true(posts.isLoaded, 'The LiveArray is loaded'); assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); @@ -1085,7 +1219,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }); await store.query('post', { page: 1 }); - let comment = store.peekRecord('comment', 1); + const comment = store.peekRecord('comment', 1); assert.deepEqual(comment.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); }); @@ -1117,16 +1251,16 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let posts = await store.query('post', { page: 1 }); - let post1 = store.peekRecord('post', 1); - let post2 = store.peekRecord('post', 2); + const posts = await store.query('post', { page: 1 }); + const post1 = store.peekRecord('post', 1); + const post2 = store.peekRecord('post', 2); assert.deepEqual(post1.getProperties('id', 'name'), { id: '1', name: 'Rails is omakase' }, 'Post 1 is loaded'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }, 'Post 2 is loaded'); assert.strictEqual(posts.length, 2, 'The posts are in the array'); - assert.true(posts.isLoaded, 'The RecordArray is loaded'); + assert.true(posts.isLoaded, 'The LiveArray is loaded'); assert.deepEqual(posts.slice(), [post1, post2], 'The correct records are in the array'); }); @@ -1144,7 +1278,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ajaxResponse({}); - let post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); + const post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); assert.strictEqual(post, null); }); @@ -1164,7 +1298,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { post: null, }); - let post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); + const post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); assert.strictEqual(post, null); }); @@ -1187,7 +1321,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); + const post = await store.queryRecord('post', { slug: 'ember-js-rocks' }); assert.deepEqual(post.name, 'Ember.js rocks'); }); @@ -1209,7 +1343,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }); await store.queryRecord('post', { slug: 'rails-is-omakaze' }); - let comment = store.peekRecord('comment', 1); + const comment = store.peekRecord('comment', 1); assert.deepEqual(comment.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); }); @@ -1235,7 +1369,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.expectAssertion( () => store.queryRecord('post', { slug: 'rails-is-omakaze' }), - 'Assertion Failed: The adapter returned an array for the primary data of a `queryRecord` response. `queryRecord` should return a single record.' + 'The adapter returned an array for the primary data of a `queryRecord` response. `queryRecord` should return a single record.' ); }); @@ -1326,7 +1460,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); ajaxResponse({ comments: [ { id: '1', name: 'FIRST' }, @@ -1378,7 +1512,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); ajaxResponse({ comments: [ { id: '1', name: 'FIRST' }, @@ -1422,7 +1556,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); //It's still ok to return this even without coalescing because RESTSerializer supports sideloading ajaxResponse({ comments: [ @@ -1471,7 +1605,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ comments: [ { id: '1', name: 'FIRST' }, @@ -1480,10 +1614,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let comments = await post.comments; - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let comment3 = store.peekRecord('comment', 3); + const comments = await post.comments; + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const comment3 = store.peekRecord('comment', 3); assert.deepEqual(comment1.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); assert.deepEqual(comment2.getProperties('id', 'name'), { id: '2', name: 'Rails is unagi' }); @@ -1538,13 +1672,13 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: [{ id: '2', name: 'The Parley Letter' }], }); - let comments = await post.comments; + const comments = await post.comments; - let comment1 = store.peekRecord('comment', '1'); - let comment2 = store.peekRecord('comment', '2'); - let comment3 = store.peekRecord('comment', '3'); - let comment4 = store.peekRecord('comment', '4'); - let post2 = store.peekRecord('post', '2'); + const comment1 = store.peekRecord('comment', '1'); + const comment2 = store.peekRecord('comment', '2'); + const comment3 = store.peekRecord('comment', '3'); + const comment4 = store.peekRecord('comment', '4'); + const post2 = store.peekRecord('post', '2'); assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); @@ -1605,7 +1739,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ comments: [ @@ -1615,10 +1749,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let comments = await post.comments; - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let comment3 = store.peekRecord('comment', 3); + const comments = await post.comments; + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const comment3 = store.peekRecord('comment', 3); assert.deepEqual(comment1.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); assert.deepEqual(comment2.getProperties('id', 'name'), { id: '2', name: 'Rails is unagi' }); @@ -1660,7 +1794,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [ @@ -1670,14 +1804,14 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(passedUrl, '/posts/1/comments'); assert.strictEqual(passedVerb, 'GET'); assert.strictEqual(passedHash, undefined); - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let comment3 = store.peekRecord('comment', 3); + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const comment3 = store.peekRecord('comment', 3); assert.deepEqual(comment1.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); assert.deepEqual(comment2.getProperties('id', 'name'), { @@ -1696,7 +1830,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.expect(2); adapter.shouldBackgroundReloadRecord = () => false; adapter.buildURL = function (type, id, snapshot, requestType) { - assert.ok(snapshot instanceof Snapshot); + assert.ok(isSnapshot(snapshot)); assert.strictEqual(requestType, 'findHasMany'); }; @@ -1728,7 +1862,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); ajaxResponse({ comments: [ @@ -1773,7 +1907,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); ajaxResponse({ comments: [ @@ -1784,18 +1918,18 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { posts: [{ id: '2', name: 'The Parley Letter' }], }); - let comments = await post.comments; - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let comment3 = store.peekRecord('comment', 3); - let post2 = store.peekRecord('post', 2); + const comments = await post.comments; + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const comment3 = store.peekRecord('comment', 3); + const post2 = store.peekRecord('post', 2); assert.deepEqual(comments.slice(), [comment1, comment2, comment3], 'The correct records are in the array'); assert.deepEqual(post2.getProperties('id', 'name'), { id: '2', name: 'The Parley Letter' }); }); - test('findMany - a custom serializer is used if present', async function (assert) { + test('findMany - a custom serializer is used if present, v2', async function (assert) { adapter.shouldBackgroundReloadRecord = () => false; this.owner.register( 'serializer:post', @@ -1854,9 +1988,9 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return post.comments; }) .then((comments) => { - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let comment3 = store.peekRecord('comment', 3); + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const comment3 = store.peekRecord('comment', 3); assert.deepEqual(comment1.getProperties('id', 'name'), { id: '1', name: 'FIRST' }); assert.deepEqual(comment2.getProperties('id', 'name'), { id: '2', name: 'Rails is unagi' }); @@ -1884,7 +2018,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.expect(2); adapter.shouldBackgroundReloadRecord = () => false; adapter.buildURL = function (type, id, snapshot, requestType) { - assert.ok(snapshot instanceof Snapshot); + assert.ok(isSnapshot(snapshot)); assert.strictEqual(requestType, 'findBelongsTo'); }; @@ -1905,7 +2039,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let comment = await store.findRecord('comment', '1'); + const comment = await store.findRecord('comment', '1'); ajaxResponse({ post: { id: '1', name: 'Rails is omakase' } }); await comment.post; }); @@ -1931,7 +2065,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { comments: [{ id: '1', type: 'comment' }], }); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '2', @@ -1986,12 +2120,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(id, '1'); - return resolve({ comments: { id: '1' } }); + return Promise.resolve({ comments: { id: '1' } }); }; adapter.findMany = function (store, type, ids, snapshots) { assert.deepEqual(ids, ['2', '3']); - return resolve({ comments: [{ id: '2' }, { id: '3' }] }); + return Promise.resolve({ comments: [{ id: '2' }, { id: '3' }] }); }; store.push({ @@ -2010,7 +2144,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 2); + const post = store.peekRecord('post', 2); await post.comments; }); @@ -2038,12 +2172,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(id, '1'); - return resolve({ comments: { id: '1' } }); + return Promise.resolve({ comments: { id: '1' } }); }; adapter.findMany = function (store, type, ids, snapshots) { assert.deepEqual(ids, ['2', '3']); - return resolve({ comments: [{ id: '2' }, { id: '3' }] }); + return Promise.resolve({ comments: [{ id: '2' }, { id: '3' }] }); }; store.push({ @@ -2062,7 +2196,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - let post = store.peekRecord('post', 2); + const post = store.peekRecord('post', 2); await post.comments; }); @@ -2097,10 +2231,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { keyForRelationship(rel, kind) { if (kind === 'belongsTo') { - let underscored = underscore(rel); + const underscored = underscore(rel); return underscored + '_id'; } else { - let singular = singularize(rel); + const singular = singularize(rel); return underscore(singular) + '_ids'; } }, @@ -2137,7 +2271,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let post = await store.findRecord('post', 1); + const post = await store.findRecord('post', 1); assert.strictEqual(post.authorName, '@d2h'); assert.strictEqual(post.author.name, 'D2H'); assert.deepEqual( @@ -2163,9 +2297,8 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return new Array(n + 1).join(character); } - let a2000 = repeatChar('a', 2000); - let b2000 = repeatChar('b', 2000); - let post; + const a2000 = repeatChar('a', 2000); + const b2000 = repeatChar('b', 2000); store.push({ data: { @@ -2182,7 +2315,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); adapter.coalesceFindRequests = true; @@ -2191,12 +2324,12 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.ok(true, 'Found ' + id); } - return resolve({ comments: { id: id } }); + return Promise.resolve({ comments: { id: id } }); }; adapter.findMany = function (store, type, ids, snapshots) { assert.ok(false, 'findMany should not be called - we expect 2 calls to find for a2000 and b2000'); - return reject(); + return Promise.reject(); }; post.comments; @@ -2220,9 +2353,8 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return new Array(n + 1).join(character); } - let a100 = repeatChar('a', 100); - let b100 = repeatChar('b', 100); - let post; + const a100 = repeatChar('a', 100); + const b100 = repeatChar('b', 100); store.push({ data: { @@ -2239,18 +2371,18 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { }, }); - post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); adapter.coalesceFindRequests = true; adapter.findRecord = function (store, type, id, snapshot) { assert.ok(false, 'findRecord should not be called - we expect 1 call to findMany for a100 and b100'); - return reject(); + return Promise.reject(); }; adapter.findMany = function (store, type, ids, snapshots) { assert.deepEqual(ids, [a100, b100]); - return resolve({ comments: [{ id: a100 }, { id: b100 }] }); + return Promise.resolve({ comments: [{ id: a100 }, { id: b100 }] }); }; await post.comments; @@ -2270,7 +2402,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.expect(2); - let data = { + const data = { post: { id: '1', name: 'Docker is amazing', @@ -2303,9 +2435,9 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { this.owner.register('model:comment', Comment); assert.expect(4); - let responseText = 'Nope lol'; + const responseText = 'Nope lol'; - let expectedRequestData = { + const expectedRequestData = { method: 'GET', url: '/posts/1', }; @@ -2342,7 +2474,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { assert.expect(4); - let data = { + const data = { something: 'is invalid', }; @@ -2388,7 +2520,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { try { await store.findRecord('post', '1'); - } catch (error) { + } catch { assert.ok(true, 'Unexpected error is captured by the promise chain'); } }); @@ -2417,7 +2549,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { try { await store.findRecord('post', '1'); - } catch (error) { + } catch { assert.ok(true, 'Unexpected error is captured by the promise chain'); } }); @@ -2446,7 +2578,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { } catch (err) { assert.ok(err instanceof AbortError, 'reason should be an instance of AbortError'); assert.strictEqual(err.errors.length, 1, 'AbortError includes errors with request/response details'); - let expectedError = { + const expectedError = { title: 'Adapter Error', detail: 'Request failed: GET /posts/1', status: 0, @@ -2468,14 +2600,14 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { } this.owner.register('model:comment', Comment); - let jqXHR = { + const jqXHR = { responseText: 'Nope lol', getAllResponseHeaders() { return ''; }, }; - let errorThrown = new Error('nope!'); + const errorThrown = new Error('nope!'); adapter.useFetch = false; adapter._ajaxRequest = function (hash) { @@ -2510,14 +2642,14 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { } this.owner.register('model:comment', Comment); - let jqXHR = { + const jqXHR = { responseText: '', getAllResponseHeaders() { return ''; }, }; - let errorThrown = 'nope!'; + const errorThrown = 'nope!'; adapter.useFetch = false; adapter._ajaxRequest = function (hash) { @@ -2675,7 +2807,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let posts = await store.findAll('post'); + const posts = await store.findAll('post'); assert.strictEqual(get(posts, 'length'), 3); posts.forEach((post) => assert.ok(post instanceof Model)); }); @@ -2710,10 +2842,10 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { ], }); - let post = store.createRecord('post', { name: 'The Parley Letter' }); + const post = store.createRecord('post', { name: 'The Parley Letter' }); await post.save(); - let comments = store.peekAll('comment'); + const comments = store.peekAll('comment'); assert.strictEqual(comments.length, 2, 'comments.length is correct'); assert.strictEqual(comments[0].name, 'First comment', 'comments.at(0).name is correct'); @@ -2740,7 +2872,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return [201, { 'Content-Type': 'application/json' }, '']; }); - let post = store.createRecord('post'); + const post = store.createRecord('post'); return post.save().then( () => { assert.strictEqual(true, false, 'should not have fulfilled'); @@ -2773,7 +2905,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return [201, { 'Content-Type': 'application/json' }, '']; }); - let post = store.createRecord('post'); + const post = store.createRecord('post'); return post.save().then( () => { assert.equal(true, false, 'should not have fulfilled'); @@ -2805,7 +2937,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return [200, { 'Content-Type': 'application/json' }, '']; }); - let post = store.push({ data: { id: '1', type: 'post' } }); + const post = store.push({ data: { id: '1', type: 'post' } }); await assert.expectWarning(async () => { return post.save().then(() => assert.ok(true, 'save fullfills correctly')); }, /JSON/); @@ -2830,7 +2962,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return [200, { 'Content-Type': 'application/json' }, '']; }); - let post = store.push({ data: { id: '1', type: 'post' } }); + const post = store.push({ data: { id: '1', type: 'post' } }); return post.save().then(() => assert.ok(true, 'save fullfills correctly')); }); @@ -2852,7 +2984,7 @@ module('integration/adapter/rest_adapter - REST Adapter', function (hooks) { return [200, { 'Content-Type': 'application/json' }, null]; }); - let post = store.push({ data: { id: '1', type: 'post' } }); + const post = store.push({ data: { id: '1', type: 'post' } }); return post.save().then(() => assert.ok(true, 'save fullfills correctly')); }); }); diff --git a/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js b/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js index 8eddebea5e0..27cf5bff835 100644 --- a/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js +++ b/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js @@ -1,13 +1,9 @@ -import { resolve } from 'rsvp'; - -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; - /** * @description Helper function to mock the response of an adapter in order to - * Test is behaviour. + * test its behavior. * @param { adapter } RESTAdapter instance * @param { response } Response to return from the adapter - * @returns { ajaxCallback } Function that returns information about the last + * @return { ajaxCallback } Function that returns information about the last * call to the ajax method of the adapter. */ function ajaxResponse(adapter, value) { @@ -19,9 +15,9 @@ function ajaxResponse(adapter, value) { passedHash = hash; passedUrl = passedHash.url; passedVerb = passedHash.method; - return resolve({ + return Promise.resolve({ text() { - return resolve(JSON.stringify(deepCopy(value))); + return Promise.resolve(JSON.stringify(structuredClone(value))); }, ok: true, status: 200, @@ -33,7 +29,7 @@ function ajaxResponse(adapter, value) { passedVerb = verb; passedHash = hash; - return resolve(deepCopy(value)); + return Promise.resolve(structuredClone(value)); }; return () => { diff --git a/tests/main/tests/integration/adapter/rest-adapter/create-record-test.js b/tests/main/tests/integration/adapter/rest-adapter/create-record-test.js index c38243ab30a..9c0c769a653 100644 --- a/tests/main/tests/integration/adapter/rest-adapter/create-record-test.js +++ b/tests/main/tests/integration/adapter/rest-adapter/create-record-test.js @@ -3,8 +3,10 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import JSONAPIAdapter from '@ember-data/adapter/json-api'; import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; import { ajaxResponse } from './-ajax-mocks'; @@ -477,7 +479,7 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio const commentCount = post.comments.length; assert.strictEqual(commentCount, 1, 'the post starts life with a comment'); - let comment = store.createRecord('comment', { name: 'Another Comment', post: post }); + const comment = store.createRecord('comment', { name: 'Another Comment', post: post }); await comment.save(); assert.strictEqual(comment.post, post, 'the comment is related to the post'); @@ -522,39 +524,53 @@ module('integration/adapter/rest_adapter - REST Adapter - createRecord', functio test("createRecord - response can contain relationships the client doesn't yet know about", async function (assert) { assert.expect(3); // while recorlength is 2, we are getting 4 assertions - const Post = Model.extend({ - name: attr('string'), - comments: hasMany('comment', { async: false, inverse: 'post' }), - }); - const Comment = Model.extend({ - name: attr('string'), - post: belongsTo('post', { async: false, inverse: 'comments' }), - }); + class Post extends Model { + @attr('string') name; + @hasMany('comment', { async: false, inverse: 'post' }) comments; + } + class Comment extends Model { + @attr('string') name; + @belongsTo('post', { async: false, inverse: 'comments' }) post; + } this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - this.owner.register('adapter:application', RESTAdapter.extend()); - this.owner.register('serializer:application', RESTSerializer.extend()); + this.owner.register('adapter:application', JSONAPIAdapter); + this.owner.register('serializer:application', JSONAPISerializer); const store = this.owner.lookup('service:store'); const adapter = store.adapterFor('application'); - ajaxResponse(adapter, { - posts: [ - { + adapter.createRecord = function () { + return { + data: { id: '1', - name: 'Rails is omakase', - comments: ['2'], - }, - ], - comments: [ - { - id: '2', - name: 'Another Comment', - post: '1', + type: 'post', + attributes: { + name: 'Rails is omakase', + }, + relationships: { + comments: { + data: [{ type: 'comment', id: '1' }], + }, + }, }, - ], - }); + included: [ + { + id: '1', + type: 'comment', + attributes: { + name: 'Dat Parlay Letter', + }, + relationships: { + post: { + data: { type: 'post', id: '1' }, + }, + }, + }, + ], + }; + }; const post = store.createRecord('post', { name: 'Rails is omakase' }); diff --git a/tests/main/tests/integration/adapter/rest-adapter/find-record-test.js b/tests/main/tests/integration/adapter/rest-adapter/find-record-test.js index 294b3865afd..c026019d62d 100644 --- a/tests/main/tests/integration/adapter/rest-adapter/find-record-test.js +++ b/tests/main/tests/integration/adapter/rest-adapter/find-record-test.js @@ -86,7 +86,7 @@ module('integration/adapter/rest_adapter - REST Adapter - findRecord', function this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let id = '1'; + const id = '1'; const store = this.owner.lookup('service:store'); const adapter = store.adapterFor('application'); const ajaxCallback = ajaxResponse(adapter, { posts: [{ id, name: 'Rails is omakase' }] }); @@ -120,10 +120,10 @@ module('integration/adapter/rest_adapter - REST Adapter - findRecord', function assert.strictEqual(post.name, 'Rails is omakase'); // stress tests - let peekPost = store.peekRecord(findRecordArgs); + const peekPost = store.peekRecord(findRecordArgs); assert.strictEqual(peekPost, post, 'peekRecord returns same post'); - let recordReference = store.getReference(findRecordArgs); + const recordReference = store.getReference(findRecordArgs); assert.strictEqual(recordReference.remoteType(), 'identity'); assert.strictEqual(recordReference.type, 'post'); assert.strictEqual(recordReference.id(), '1'); @@ -147,10 +147,10 @@ module('integration/adapter/rest_adapter - REST Adapter - findRecord', function assert.strictEqual(record, foundPost, 'We were able to findRecord'); // stress tests - let peekPost = store.peekRecord(identifier); + const peekPost = store.peekRecord(identifier); assert.strictEqual(peekPost, foundPost, 'peekRecord returns same post'); - let recordReference = store.getReference(identifier); + const recordReference = store.getReference(identifier); assert.strictEqual(recordReference.remoteType(), 'identity'); assert.strictEqual(recordReference.type, 'post'); assert.strictEqual(recordReference.id(), null); @@ -173,7 +173,7 @@ module('integration/adapter/rest_adapter - REST Adapter - findRecord', function await assert.expectAssertion(async () => { await store.findRecord(identifier); - }, 'Assertion Failed: Attempted to schedule a fetch for a record without an id.'); + }, 'Attempted to schedule a fetch for a record without an id.'); } ); @@ -200,7 +200,7 @@ module('integration/adapter/rest_adapter - REST Adapter - findRecord', function await assert.expectAssertion(async () => { await store.findRecord(identifier, options); - }, 'Assertion Failed: Attempted to schedule a fetch for a record without an id.'); + }, 'Attempted to schedule a fetch for a record without an id.'); } ); }); diff --git a/tests/main/tests/integration/adapter/serialize-test.js b/tests/main/tests/integration/adapter/serialize-test.js index 26df9512339..d88f7714bd2 100644 --- a/tests/main/tests/integration/adapter/serialize-test.js +++ b/tests/main/tests/integration/adapter/serialize-test.js @@ -18,15 +18,16 @@ module('integration/adapter/serialize - DS.Adapter integration test', function ( this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const serializer = store.serializerFor('application'); serializer.serialize = function (snapshot, options) { assert.deepEqual(options, { foo: 'bar' }); + return {}; }; - let person = store.createRecord('person'); + const person = store.createRecord('person'); adapter.serialize(person._createSnapshot(), { foo: 'bar' }); }); diff --git a/tests/main/tests/integration/adapter/store-adapter-test.js b/tests/main/tests/integration/adapter/store-adapter-test.js index e5ca30b6a85..c363f623a9d 100644 --- a/tests/main/tests/integration/adapter/store-adapter-test.js +++ b/tests/main/tests/integration/adapter/store-adapter-test.js @@ -1,7 +1,6 @@ import { get, set } from '@ember/object'; import { module, test } from 'qunit'; -import { hash, Promise as EmberPromise, reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -15,14 +14,18 @@ import RESTSerializer from '@ember-data/serializer/rest'; import { recordIdentifierFor } from '@ember-data/store'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +function isSnapshot(snapshot) { + return snapshot instanceof Snapshot || snapshot.constructor.name === 'Snapshot'; +} + function moveRecordOutOfInFlight(record) { // move record out of the inflight state so the tests can clean up // correctly - let { store } = record; - let identifier = recordIdentifierFor(record); + const { store } = record; + const identifier = recordIdentifierFor(record); // TODO this would be made nicer by a cancellation API - let pending = store.getRequestStateService().getPendingRequestsForRecord(identifier); + const pending = store.getRequestStateService().getPendingRequestsForRecord(identifier); pending.splice(0, pending.length); } @@ -46,11 +49,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('Records loaded multiple times and retrieved in recordArray are ready to send state events', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function (store, type, query, recordArray) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -83,9 +86,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('by default, createRecords calls createRecord once per record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let count = 1; adapter.shouldBackgroundReloadRecord = () => false; @@ -100,12 +103,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.ok(false, 'should not have invoked more than 2 times'); } - let hash = snapshot.attributes(); - let recordId = count; + const hash = snapshot.attributes(); + const recordId = count; hash['updated-at'] = 'now'; count++; - return resolve({ + return Promise.resolve({ data: { id: recordId, type: 'person', @@ -117,14 +120,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration let tom = store.createRecord('person', { name: 'Tom Dale' }); let yehuda = store.createRecord('person', { name: 'Yehuda Katz' }); - let promise = hash({ - tom: tom.save(), - yehuda: yehuda.save(), - }); - - let records = await promise; - tom = records.tom; - yehuda = records.yehuda; + [tom, yehuda] = await Promise.all([tom.save(), yehuda.save()]); assert.strictEqual( tom, @@ -141,9 +137,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('by default, updateRecords calls updateRecord once per record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let count = 0; adapter.shouldBackgroundReloadRecord = () => false; @@ -162,7 +158,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.true(snapshot.record.isSaving, 'record is saving'); - return resolve(); + return Promise.resolve(); }; store.push({ @@ -184,25 +180,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ], }); - let promise = hash({ - tom: store.findRecord('person', '1'), - yehuda: store.findRecord('person', '2'), - }); - - let records1 = await promise; - let tom = records1.tom; - let yehuda = records1.yehuda; + const [tom, yehuda] = await Promise.all([store.findRecord('person', '1'), store.findRecord('person', '2')]); set(tom, 'name', 'Tom Dale'); set(yehuda, 'name', 'Yehuda Katz'); - let records2 = await hash({ - tom: tom.save(), - yehuda: yehuda.save(), - }); - - let tom2 = records2.tom; - let yehuda2 = records2.yehuda; + const [tom2, yehuda2] = await Promise.all([tom.save(), yehuda.save()]); assert.false(tom2.isSaving, 'record is no longer saving'); assert.true(tom2.isLoaded, 'record is loaded'); @@ -212,9 +195,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('additional new values can be returned on store save', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let count = 0; adapter.shouldBackgroundReloadRecord = () => false; @@ -224,12 +207,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration count++; if (count === 1) { assert.strictEqual(snapshot.attr('name'), 'Tom Dale'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Tom Dale', 'updated-at': 'now' } }, }); } else if (count === 2) { assert.strictEqual(snapshot.attr('name'), 'Yehuda Katz'); - return resolve({ + return Promise.resolve({ data: { id: '2', type: 'person', @@ -278,9 +261,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('by default, deleteRecord calls deleteRecord once per record', async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let count = 0; adapter.shouldBackgroundReloadRecord = () => false; @@ -297,7 +280,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration count++; - return resolve(); + return Promise.resolve(); }; store.push({ @@ -319,27 +302,20 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ], }); - let promise = hash({ - tom: store.findRecord('person', '1'), - yehuda: store.findRecord('person', '2'), - }); - - let records = await promise; - let tom = records.tom; - let yehuda = records.yehuda; + const [tom, yehuda] = await Promise.all([store.findRecord('person', '1'), store.findRecord('person', '2')]); tom.deleteRecord(); yehuda.deleteRecord(); - return EmberPromise.all([tom.save(), yehuda.save()]); + await Promise.all([tom.save(), yehuda.save()]); }); test('by default, destroyRecord calls deleteRecord once per record without requiring .save', async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let count = 0; @@ -357,7 +333,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration count++; - return resolve(); + return Promise.resolve(); }; store.push({ @@ -379,23 +355,16 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ], }); - let promise = hash({ - tom: store.findRecord('person', '1'), - yehuda: store.findRecord('person', '2'), - }); - - let records = await promise; - let tom = records.tom; - let yehuda = records.yehuda; + const [tom, yehuda] = await Promise.all([store.findRecord('person', '1'), store.findRecord('person', '2')]); - return EmberPromise.all([tom.destroyRecord(), yehuda.destroyRecord()]); + await Promise.all([tom.destroyRecord(), yehuda.destroyRecord()]); }); test('if an existing model is edited then deleted, deleteRecord is called on the adapter', async function (assert) { assert.expect(5); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let count = 0; adapter.shouldBackgroundReloadRecord = () => false; @@ -404,7 +373,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.strictEqual(snapshot.id, 'deleted-record', 'should pass correct record to deleteRecord'); assert.strictEqual(count, 1, 'should only call deleteRecord method of adapter once'); - return resolve(); + return Promise.resolve(); }; adapter.updateRecord = function () { @@ -435,18 +404,18 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if a deleted record errors, it enters the error state', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let count = 0; - let error = new AdapterError(); + const error = new AdapterError(); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = function (store, type, snapshot) { if (count++ === 0) { - return reject(error); + return Promise.reject(error); } else { - return resolve(); + return Promise.resolve(); } }; @@ -465,7 +434,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration try { await tom.save(); assert.ok(false, 'We should throw during save'); - } catch (e) { + } catch { assert.true(tom.isError, 'Tom is now errored'); assert.strictEqual(tom.adapterError, error, 'error object is exposed'); @@ -478,15 +447,15 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if a created record is marked as invalid by the server, it enters an error state', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); adapter.createRecord = function (store, type, snapshot) { assert.strictEqual(type, Person, 'the type is correct'); if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -498,18 +467,18 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); + const yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); // Wrap this in an Ember.run so that all chained async behavior is set up // before flushing any scheduled behavior. try { await yehuda.save(); assert.ok(false, 'We should have erred'); - } catch (e) { + } catch { assert.false(yehuda.isValid, 'the record is invalid'); assert.ok(get(yehuda, 'errors.name'), 'The errors.name property exists'); @@ -523,7 +492,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.true(yehuda.isNew, 'precond - record is still new'); - let person = await yehuda.save(); + const person = await yehuda.save(); assert.strictEqual(person, yehuda, 'The promise resolves with the saved record'); @@ -533,12 +502,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('allows errors on arbitrary properties on create', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, type, snapshot) { if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -550,16 +519,16 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); + const yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); // Wrap this in an Ember.run so that all chained async behavior is set up // before flushing any scheduled behavior. - let person = await yehuda.save().catch(() => { + const person = await yehuda.save().catch(() => { assert.false(yehuda.isValid, 'the record is invalid'); assert.ok(get(yehuda, 'errors.base'), 'The errors.base property exists'); assert.deepEqual(get(yehuda, 'errors').errorsFor('base'), [ @@ -587,9 +556,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if a created record is marked as invalid by the server, you can attempt the save again', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let saveCount = 0; adapter.createRecord = function (store, type, snapshot) { @@ -597,7 +566,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration saveCount++; if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -609,16 +578,16 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); + const yehuda = store.createRecord('person', { id: '1', name: 'Yehuda Katz' }); // Wrap this in an Ember.run so that all chained async behavior is set up // before flushing any scheduled behavior. - let person = await yehuda + const person = await yehuda .save() .catch((reason) => { assert.strictEqual(saveCount, 1, 'The record has been saved once'); @@ -653,16 +622,16 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if a created record is marked as erred by the server, it enters an error state', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let error = new AdapterError(); + const error = new AdapterError(); adapter.createRecord = function (store, type, snapshot) { - return reject(error); + return Promise.reject(error); }; - let person = store.createRecord('person', { id: '1', name: 'John Doe' }); + const person = store.createRecord('person', { id: '1', name: 'John Doe' }); return person.save().catch(() => { assert.ok(person.isError, 'the record is in the error state'); @@ -671,16 +640,16 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if an updated record is marked as invalid by the server, it enters an error state', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = function (store, type, snapshot) { assert.strictEqual(type, Person, 'the type is correct'); if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -692,11 +661,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.push({ + const yehuda = store.push({ data: { type: 'person', id: '1', @@ -708,7 +677,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration store.peekRecord('person', '1'); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.strictEqual(person, yehuda, 'The same object is passed through'); assert.true(yehuda.isValid, 'precond - the record is valid'); @@ -717,8 +686,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.true(yehuda.hasDirtyAttributes, 'the record is dirty'); - let reason = yehuda.save(); - let response = await reason.catch(() => { + const reason = yehuda.save(); + const response = await reason.catch(() => { assert.true(yehuda.hasDirtyAttributes, 'the record is still dirty'); assert.false(yehuda.isValid, 'the record is invalid'); @@ -736,13 +705,13 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('records can have errors on arbitrary properties after update', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = function (store, type, snapshot) { if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -754,11 +723,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.push({ + const yehuda = store.push({ data: { type: 'person', id: '1', @@ -769,7 +738,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); store.peekRecord('person', '1'); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.strictEqual(person, yehuda, 'The same object is passed through'); @@ -779,8 +748,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.true(yehuda.hasDirtyAttributes, 'the record is dirty'); - let reason = yehuda.save(); - let response = await reason.catch(() => { + const reason = yehuda.save(); + const response = await reason.catch(() => { assert.true(yehuda.hasDirtyAttributes, 'the record is still dirty'); assert.false(yehuda.isValid, 'the record is invalid'); assert.ok(get(yehuda, 'errors.base'), 'The errors.base property exists'); @@ -807,9 +776,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if an updated record is marked as invalid by the server, you can attempt the save again', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); let saveCount = 0; adapter.shouldBackgroundReloadRecord = () => false; @@ -817,7 +786,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.strictEqual(type, Person, 'the type is correct'); saveCount++; if (snapshot.attr('name').indexOf('Bro') === -1) { - return reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -829,11 +798,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration ]) ); } else { - return resolve(); + return Promise.resolve(); } }; - let yehuda = store.push({ + const yehuda = store.push({ data: { type: 'person', id: '1', @@ -844,7 +813,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); store.peekRecord('person', '1'); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.strictEqual(person, yehuda, 'The same object is passed through'); assert.true(yehuda.isValid, 'precond - the record is valid'); @@ -853,8 +822,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration assert.true(yehuda.hasDirtyAttributes, 'the record is dirty'); - let reason = yehuda.save(); - let response = await reason + const reason = yehuda.save(); + const response = await reason .catch((reason) => { assert.strictEqual(saveCount, 1, 'The record has been saved once'); assert.ok( @@ -884,17 +853,17 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test('if a updated record is marked as erred by the server, it enters an error state', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let error = new AdapterError(); + const error = new AdapterError(); adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = function (store, type, snapshot) { - return reject(error); + return Promise.reject(error); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -905,11 +874,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); store.peekRecord('person', '1'); - let record = await store.findRecord('person', '1'); + const record = await store.findRecord('person', '1'); assert.strictEqual(record, person, 'The person was resolved'); person.set('name', 'Jonathan Doe'); - let reason = person.save(); + const reason = person.save(); reason.catch(() => { assert.ok(person.isError, 'the record is in the error state'); assert.strictEqual(person.adapterError, error, 'error object is exposed'); @@ -919,21 +888,21 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('can be created after the Store', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Person = store.modelFor('person'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(type, Person, 'the type is correct'); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; await store.findRecord('person', '1'); }); test('relationships returned via `commit` do not trigger additional findManys', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @hasMany('dog', { async: false, inverse: null }) dogs; @@ -954,7 +923,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -969,36 +938,34 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }; adapter.updateRecord = function (store, type, snapshot) { - return new EmberPromise((resolve, reject) => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - dogs: { - data: [ - { type: 'dog', id: '1' }, - { type: 'dog', id: '2' }, - ], - }, + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + dogs: { + data: [ + { type: 'dog', id: '1' }, + { type: 'dog', id: '2' }, + ], }, }, - included: [ - { - type: 'dog', - id: '2', - attributes: { - name: 'Scruffles', - }, + }, + included: [ + { + type: 'dog', + id: '2', + attributes: { + name: 'Scruffles', }, - ], - }); - - resolve({ data: { id: '1', type: 'dog', attributes: { name: 'Scruffy' } } }); + }, + ], }); + + return Promise.resolve({ data: { id: '1', type: 'dog', attributes: { name: 'Scruffy' } } }); }; adapter.findMany = function (store, type, ids, snapshots) { @@ -1014,8 +981,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); test("relationships don't get reset if the links is the same", async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @hasMany('dog', { async: true, inverse: null }) dogs; @@ -1029,7 +996,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration adapter.findHasMany = function (store, snapshot, link, relationship) { assert.strictEqual(count++, 0, 'findHasMany is only called once'); - return resolve({ data: [{ id: '1', type: 'dog', attributes: { name: 'Scruffy' } }] }); + return Promise.resolve({ data: [{ id: '1', type: 'dog', attributes: { name: 'Scruffy' } }] }); }; store.push({ @@ -1049,11 +1016,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); - let tom = person; - let dogs = tom.dogs; - let record = await dogs; + const tom = person; + const dogs = tom.dogs; + const record = await dogs; assert.strictEqual(record.length, 1, 'The dogs are loaded'); store.push({ @@ -1073,14 +1040,14 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }); assert.strictEqual(typeof tom.dogs.then, 'function', 'dogs is a thenable'); - let record2 = await tom.dogs; + const record2 = await tom.dogs; assert.strictEqual(record2.length, 1, 'The same dogs are loaded'); }); test('async hasMany always returns a promise', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @hasMany('dog', { async: true, inverse: null }) dogs; } @@ -1088,7 +1055,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration this.owner.register('model:person', Person); adapter.createRecord = function (store, type, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -1102,7 +1069,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); }; - let tom = store.createRecord('person', { name: 'Tom Dale' }); + const tom = store.createRecord('person', { name: 'Tom Dale' }); assert.strictEqual(typeof tom.dogs.then, 'function', 'dogs is a thenable before save'); @@ -1113,15 +1080,15 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('createRecord receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, type, snapshot) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve(); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve(); }; - let record = store.createRecord('person', { name: 'Tom Dale', id: '1' }); + const record = store.createRecord('person', { name: 'Tom Dale', id: '1' }); await record.save(); }); @@ -1129,16 +1096,14 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('updateRecord receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (store, type, snapshot) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve(); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve(); }; - let person; - store.push({ data: { type: 'person', @@ -1148,7 +1113,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); set(person, 'name', 'Tomster'); await person.save(); @@ -1157,16 +1122,14 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('deleteRecord receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (store, type, snapshot) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve(); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve(); }; - let person; - store.push({ data: { type: 'person', @@ -1176,7 +1139,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); person.deleteRecord(); await person.save(); @@ -1185,12 +1148,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('findRecord receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve({ data: { id: '1', type: 'person' } }); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; await store.findRecord('person', '1'); @@ -1199,8 +1162,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('findMany receives an array of snapshots', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @hasMany('dog', { async: true, inverse: null }) dogs; } @@ -1209,9 +1172,9 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration adapter.coalesceFindRequests = true; adapter.findMany = function (store, type, ids, snapshots) { - assert.ok(snapshots[0] instanceof Snapshot, 'snapshots[0] is an instance of Snapshot'); - assert.ok(snapshots[1] instanceof Snapshot, 'snapshots[1] is an instance of Snapshot'); - return resolve({ + assert.ok(isSnapshot(snapshots[0]), 'snapshots[0] is an instance of Snapshot'); + assert.ok(isSnapshot(snapshots[1]), 'snapshots[1] is an instance of Snapshot'); + return Promise.resolve({ data: [ { id: '2', type: 'dog' }, { id: '3', type: 'dog' }, @@ -1219,8 +1182,6 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); }; - let person; - store.push({ data: { type: 'person', @@ -1235,7 +1196,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); await person.dogs; }); @@ -1243,8 +1204,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('findHasMany receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @hasMany('dog', { async: true, inverse: null }) dogs; } @@ -1252,8 +1213,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration this.owner.register('model:person', Person); adapter.findHasMany = function (store, snapshot, link, relationship) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve({ + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve({ data: [ { id: '2', type: 'dog' }, { id: '3', type: 'dog' }, @@ -1261,8 +1222,6 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }); }; - let person; - store.push({ data: { type: 'person', @@ -1276,7 +1235,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); await person.dogs; }); @@ -1284,8 +1243,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('findBelongsTo receives a snapshot', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Person extends Model { @belongsTo('dog', { async: true, inverse: null }) dog; @@ -1294,12 +1253,10 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration this.owner.register('model:person', Person); adapter.findBelongsTo = function (store, snapshot, link, relationship) { - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); - return resolve({ data: { id: '2', type: 'dog' } }); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); + return Promise.resolve({ data: { id: '2', type: 'dog' } }); }; - let person; - store.push({ data: { type: 'person', @@ -1313,7 +1270,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); await person.dog; }); @@ -1321,12 +1278,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('record.save should pass adapterOptions to the updateRecord method', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (store, type, snapshot) { assert.deepEqual(snapshot.adapterOptions, { subscribe: true }); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; store.push({ @@ -1338,19 +1295,19 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - let person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); await person.save({ adapterOptions: { subscribe: true } }); }); test('record.save should pass adapterOptions to the createRecord method', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, type, snapshot) { assert.deepEqual(snapshot.adapterOptions, { subscribe: true }); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; await store.createRecord('person', { name: 'Tom' }).save({ adapterOptions: { subscribe: true } }); @@ -1359,12 +1316,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('record.save should pass adapterOptions to the deleteRecord method', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (store, type, snapshot) { assert.deepEqual(snapshot.adapterOptions, { subscribe: true }); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; store.push({ @@ -1376,19 +1333,19 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - let person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); await person.destroyRecord({ adapterOptions: { subscribe: true } }); }); test('store.findRecord should pass adapterOptions to adapter.findRecord', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.deepEqual(snapshot.adapterOptions, { query: { embed: true } }); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; await store.findRecord('person', '1', { adapterOptions: { query: { embed: true } } }); @@ -1397,8 +1354,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('store.query should pass adapterOptions to adapter.query ', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function (store, type, query, array, options) { assert.notOk('adapterOptions' in query); @@ -1412,8 +1369,8 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('store.queryRecord should pass adapterOptions to adapter.queryRecord', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.queryRecord = function (store, type, query, snapshot) { assert.notOk('adapterOptions' in query); @@ -1427,12 +1384,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test("store.findRecord should pass 'include' to adapter.findRecord", async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = (store, type, id, snapshot) => { assert.strictEqual(snapshot.include, 'books', 'include passed to adapter.findRecord'); - return resolve({ data: { id: '1', type: 'person' } }); + return Promise.resolve({ data: { id: '1', type: 'person' } }); }; await store.findRecord('person', '1', { include: 'books' }); @@ -1441,13 +1398,13 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test('store.findAll should pass adapterOptions to the adapter.findAll method', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findAll = function (store, type, sinceToken, arraySnapshot) { - let adapterOptions = arraySnapshot.adapterOptions; + const adapterOptions = arraySnapshot.adapterOptions; assert.deepEqual(adapterOptions, { query: { embed: true } }); - return resolve({ data: [{ id: '1', type: 'person' }] }); + return Promise.resolve({ data: [{ id: '1', type: 'person' }] }); }; await store.findAll('person', { adapterOptions: { query: { embed: true } } }); @@ -1456,12 +1413,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration test("store.findAll should pass 'include' to adapter.findAll", async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findAll = function (store, type, sinceToken, arraySnapshot) { assert.strictEqual(arraySnapshot.include, 'books', 'include passed to adapter.findAll'); - return resolve({ data: [{ id: '1', type: 'person' }] }); + return Promise.resolve({ data: [{ id: '1', type: 'person' }] }); }; await store.findAll('person', { include: 'books' }); @@ -1488,7 +1445,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }; } findHasMany() { - return resolve({ + return Promise.resolve({ comments: [ { id: '1', name: 'FIRST' }, { id: '2', name: 'Rails is unagi' }, @@ -1506,11 +1463,11 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let post = await store.findRecord('post', '1'); + const post = await store.findRecord('post', '1'); - let comments = await post.comments; + const comments = await post.comments; assert.strictEqual(comments.length, 3); }); @@ -1518,12 +1475,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration testInDebug( 'There should be a friendly error for if the adapter does not implement createRecord', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = null; - let tom = store.createRecord('person', { name: 'Tom Dale' }); + const tom = store.createRecord('person', { name: 'Tom Dale' }); await assert.expectAssertion(async () => { await tom.save(); @@ -1536,12 +1493,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration testInDebug( 'There should be a friendly error for if the adapter does not implement updateRecord', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = null; - let tom = store.push({ data: { type: 'person', id: '1' } }); + const tom = store.push({ data: { type: 'person', id: '1' } }); await assert.expectAssertion(async () => { await tom.save(); @@ -1554,12 +1511,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration testInDebug( 'There should be a friendly error for if the adapter does not implement deleteRecord', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = null; - let tom = store.push({ data: { type: 'person', id: '1' } }); + const tom = store.push({ data: { type: 'person', id: '1' } }); await assert.expectAssertion(async () => { tom.deleteRecord(); diff --git a/tests/main/tests/integration/application-test.js b/tests/main/tests/integration/application-test.js index 0468373f9e0..35425b1f89b 100644 --- a/tests/main/tests/integration/application-test.js +++ b/tests/main/tests/integration/application-test.js @@ -6,17 +6,17 @@ import Service, { inject as service } from '@ember/service'; import { module, test } from 'qunit'; import initializeEmberData from 'ember-data/setup-container'; +import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; import Resolver from 'ember-resolver'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import Store from '@ember-data/store'; module('integration/application - Injecting a Custom Store', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.unregister('service:store'); owner.register( @@ -43,20 +43,20 @@ module('integration/application - Injecting a Custom Store', function (hooks) { }); test('If a Store property exists on an Application, it should be instantiated.', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.true(store.isCustom, 'the custom store was instantiated'); }); test('If a store is instantiated, it should be made available to each controller.', async function (assert) { ['foo', 'baz', 'application'].forEach((type) => { - let controller = this.owner.lookup(`controller:${type}`); + const controller = this.owner.lookup(`controller:${type}`); assert.true(controller.store.isCustom, 'the custom store was injected'); }); }); test('The JSONAPIAdapter is the default adapter when no custom adapter is provided', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); assert.ok(adapter instanceof JSONAPIAdapter, 'default adapter should be the JSONAPIAdapter'); }); @@ -66,7 +66,7 @@ module('integration/application - Injecting the Default Store', function (hooks) setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('controller:foo', Controller.extend({ store: service() })); owner.register( @@ -86,12 +86,12 @@ module('integration/application - Injecting the Default Store', function (hooks) }); test('If a Store property exists on an Application, it should be instantiated.', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.ok(store instanceof Store, 'the store was instantiated'); }); test('If a store is instantiated, it should be made available to each controller.', async function (assert) { - let fooController = this.owner.lookup('controller:foo'); + const fooController = this.owner.lookup('controller:foo'); assert.ok(fooController.store instanceof Store, 'the store was injected'); }); }); @@ -100,7 +100,7 @@ module('integration/application - Using the store as a service', function (hooks setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('controller:foo', Controller.extend({ store: service() })); owner.register( @@ -122,14 +122,14 @@ module('integration/application - Using the store as a service', function (hooks }); test('The store can be injected as a service', async function (assert) { - let doodleService = this.owner.lookup('service:doodle'); + const doodleService = this.owner.lookup('service:doodle'); assert.ok(doodleService.store instanceof Store, 'the store can be used as a service'); }); test('There can be multiple store services', function (assert) { - let doodleService = this.owner.lookup('service:doodle'); - let store = doodleService.store; - let secondService = this.owner.lookup('service:second-store'); + const doodleService = this.owner.lookup('service:doodle'); + const store = doodleService.store; + const secondService = this.owner.lookup('service:second-store'); assert.ok(secondService instanceof Store, 'the store can be used as a service'); assert.notStrictEqual(store, secondService, 'the store can be used as a service'); @@ -187,7 +187,7 @@ module('integration/application - Attaching initializer', function (hooks) { await this.application.boot(); this.owner = this.application.buildInstance(); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.ok( store && store.isCustomStore, 'ember-data initializer does not overwrite the previous registered service store' diff --git a/tests/main/tests/integration/backwards-compat/non-dasherized-lookups-test.js b/tests/main/tests/integration/backwards-compat/non-dasherized-lookups-test.js index ec7f280fde4..2cf3f2589ba 100644 --- a/tests/main/tests/integration/backwards-compat/non-dasherized-lookups-test.js +++ b/tests/main/tests/integration/backwards-compat/non-dasherized-lookups-test.js @@ -1,10 +1,11 @@ -import { module, test } from 'qunit'; +import { module } from 'qunit'; 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( 'integration/backwards-compat/non-dasherized-lookups - non dasherized lookups in application code finders', @@ -27,45 +28,61 @@ module( this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); - test('can lookup records using camelCase strings', async function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - - store.pushPayload('post-note', { - data: { - type: 'post-notes', - id: '1', - attributes: { - name: 'Ember Data', + deprecatedTest( + 'can lookup records using camelCase strings', + { + count: 1, + until: '6.0', + id: 'ember-data:deprecate-non-strict-types', + }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + + store.pushPayload('post-note', { + data: { + type: 'post-notes', + id: '1', + attributes: { + name: 'Ember Data', + }, }, - }, - }); - - const postNote = await store.findRecord('postNote', '1'); - - assert.strictEqual(postNote.name, 'Ember Data', 'record found'); - }); - - test('can lookup records using under_scored strings', async function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - - store.pushPayload('post-note', { - data: { - type: 'post-notes', - id: '1', - attributes: { - name: 'Ember Data', + }); + + const postNote = await store.findRecord('postNote', '1'); + + assert.strictEqual(postNote.name, 'Ember Data', 'record found'); + } + ); + + deprecatedTest( + 'can lookup records using under_scored strings', + { + count: 1, + until: '6.0', + id: 'ember-data:deprecate-non-strict-types', + }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + + store.pushPayload('post-note', { + data: { + type: 'post-notes', + id: '1', + attributes: { + name: 'Ember Data', + }, }, - }, - }); + }); - const postNote = await store.findRecord('post_note', '1'); + const postNote = await store.findRecord('post_note', '1'); - assert.strictEqual(postNote.name, 'Ember Data', 'record found'); - }); + assert.strictEqual(postNote.name, 'Ember Data', 'record found'); + } + ); } ); @@ -102,72 +119,88 @@ module( this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); - test('looks up belongsTo using camelCase strings', async function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - - store.pushPayload('post-note', { - data: { - type: 'post-notes', - id: '1', - attributes: { - name: 'Ember Data', - }, - relationships: { - 'note-post': { - data: { type: 'note-post', id: '1' }, + deprecatedTest( + 'looks up belongsTo using camelCase strings', + { + count: 2, + until: '6.0', + id: 'ember-data:deprecate-non-strict-types', + }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + + store.pushPayload('post-note', { + data: { + type: 'post-notes', + id: '1', + attributes: { + name: 'Ember Data', + }, + relationships: { + 'note-post': { + data: { type: 'note-post', id: '1' }, + }, }, }, - }, - }); - store.pushPayload('notePost', { - data: { - type: 'note-posts', - id: '1', - attributes: { - name: 'Inverse', + }); + store.pushPayload('notePost', { + data: { + type: 'note-posts', + id: '1', + attributes: { + name: 'Inverse', + }, }, - }, - }); - - const postNote = await store.findRecord('post-note', '1'); - assert.strictEqual(postNote.notePost.name, 'Inverse', 'inverse record found'); - }); - - test('looks up belongsTo using under_scored strings', async function (assert) { - assert.expect(1); - - let store = this.owner.lookup('service:store'); - - store.pushPayload('long_model_name', { - data: { - type: 'long-model-names', - id: '1', - attributes: {}, - relationships: { - 'post-notes': { - data: [{ type: 'post-note', id: '1' }], + }); + + const postNote = await store.findRecord('post-note', '1'); + assert.strictEqual(postNote.notePost.name, 'Inverse', 'inverse record found'); + } + ); + + deprecatedTest( + 'looks up belongsTo using under_scored strings', + { + count: 4, + until: '6.0', + id: 'ember-data:deprecate-non-strict-types', + }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + + store.pushPayload('long_model_name', { + data: { + type: 'long-model-names', + id: '1', + attributes: {}, + relationships: { + 'post-notes': { + data: [{ type: 'post-note', id: '1' }], + }, }, }, - }, - }); - - store.pushPayload('post-note', { - data: { - type: 'post-notes', - id: '1', - attributes: { - name: 'Ember Data', + }); + + store.pushPayload('post-note', { + data: { + type: 'post-notes', + id: '1', + attributes: { + name: 'Ember Data', + }, }, - }, - }); + }); - const longModel = await store.findRecord('long_model_name', '1'); - const postNotesRel = await longModel.postNotes; - const postNotes = postNotesRel.slice(); + const longModel = await store.findRecord('long_model_name', '1'); + const postNotesRel = await longModel.postNotes; + const postNotes = postNotesRel.slice(); - assert.deepEqual(postNotes, [store.peekRecord('postNote', '1')], 'inverse records found'); - }); + assert.deepEqual(postNotes, [store.peekRecord('postNote', '1')], 'inverse records found'); + } + ); } ); diff --git a/tests/main/tests/integration/cache-handler/lifetimes-test.ts b/tests/main/tests/integration/cache-handler/lifetimes-test.ts new file mode 100644 index 00000000000..cee301fb0b6 --- /dev/null +++ b/tests/main/tests/integration/cache-handler/lifetimes-test.ts @@ -0,0 +1,898 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import JSONAPICache from '@ember-data/json-api'; +import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import type { ImmutableRequestInfo, NextFn, RequestContext, ResponseInfo } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import { CachePolicy } from '@ember-data/request-utils'; +import type { NotificationType } from '@ember-data/store'; +import Store, { CacheHandler } from '@ember-data/store'; +import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; +import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import type { Type } from '@warp-drive/core-types/symbols'; + +type FakeRecord = { [key: string]: unknown; destroy: () => void }; + +class BaseTestStore extends Store { + createSchemaService(): SchemaService { + const schemaService: SchemaService = { + fields(identifier: StableRecordIdentifier | { type: string }): Map { + return new Map(); + }, + hasResource() { + return true; + }, + hasTrait: function (type: string): boolean { + throw new Error('Function not implemented.'); + }, + resourceHasTrait: function (resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + throw new Error('Function not implemented.'); + }, + resource: function (resource: StableRecordIdentifier | { type: string }): ResourceSchema { + throw new Error('Function not implemented.'); + }, + registerResources: function (schemas: ResourceSchema[]): void { + throw new Error('Function not implemented.'); + }, + registerResource: function (schema: ResourceSchema): void { + throw new Error('Function not implemented.'); + }, + registerTransformation: function (transform: Transformation): void { + throw new Error('Function not implemented.'); + }, + registerDerivation(derivation: Derivation): void { + throw new Error('Function not implemented.'); + }, + registerHashFn: function (hashFn: HashFn): void { + throw new Error('Function not implemented.'); + }, + transformation: function (field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + throw new Error('Function not implemented.'); + }, + hashFn: function (field: HashField | { type: string }): HashFn { + throw new Error('Function not implemented.'); + }, + derivation: function (field: DerivedField | { type: string }): Derivation { + throw new Error('Function not implemented.'); + }, + }; + + return schemaService; + } + + override createCache(wrapper: CacheCapabilitiesManager) { + return new JSONAPICache(wrapper); + } + + override instantiateRecord(identifier: StableRecordIdentifier) { + const { id, lid, type } = identifier; + const record: FakeRecord = { id, lid, type, identifier } as unknown as FakeRecord; + Object.assign(record, this.cache.peek(identifier)!.attributes); + + const token = this.notifications.subscribe( + identifier, + (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { + if (kind === 'attributes' && key) { + record[key] = this.cache.getAttr(identifier, key); + } + } + ); + + record.destroy = () => { + this.notifications.unsubscribe(token); + }; + + return record; + } + + override teardownRecord(record: FakeRecord) { + record.destroy(); + } +} + +module('Store | CacheHandler + Lifetimes', function (hooks) { + setupTest(hooks); + + test('willRequest and didRequest are not called when not present', async function (assert) { + const lifetimeIntercept = { + isHardExpired() { + assert.step('isHardExpired'); + return false; + }, + isSoftExpired() { + assert.step('isSoftExpired'); + return false; + }, + }; + const handleIntercept = { + request(_context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = lifetimeIntercept; + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + assert.verifySteps(['request issued']); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + + assert.verifySteps(['isHardExpired', 'isSoftExpired']); + }); + + test('willRequest is called if present', async function (assert) { + const lifetimeIntercept = { + willRequest() { + assert.step('willRequest'); + }, + isHardExpired() { + assert.step('isHardExpired'); + return false; + }, + isSoftExpired() { + assert.step('isSoftExpired'); + return false; + }, + }; + const handleIntercept = { + request(_context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = lifetimeIntercept; + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + assert.verifySteps(['willRequest', 'request issued']); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + + assert.verifySteps(['isHardExpired', 'isSoftExpired']); + }); + + test('didRequest is called if present', async function (assert) { + const lifetimeIntercept = { + didRequest() { + assert.step('didRequest'); + }, + isHardExpired() { + assert.step('isHardExpired'); + return false; + }, + isSoftExpired() { + assert.step('isSoftExpired'); + return false; + }, + }; + const handleIntercept = { + request(_context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = lifetimeIntercept; + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + assert.verifySteps(['request issued', 'didRequest']); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + }); + + assert.verifySteps(['isHardExpired', 'isSoftExpired']); + }); + + test('@ember-data/request-utils CachePolicy handles createRecord requests', async function (assert) { + class InterceptLifetimes extends CachePolicy { + didRequest( + request: ImmutableRequestInfo, + response: Response | ResponseInfo | null, + identifier: StableDocumentIdentifier | null, + store: Store + ): void { + assert.step('didRequest'); + super.didRequest(request, response, identifier, store); + } + isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isHardExpired(identifier, store); + assert.step(`isHardExpired: ${result}`); + return result; + } + isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isSoftExpired(identifier, store); + assert.step(`isSoftExpired: ${result}`); + return result; + } + } + const handleIntercept = { + request(context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + const response = new Response(); + response.headers.set('date', new Date().toUTCString()); + context.setResponse(response); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = new InterceptLifetimes({ + apiCacheHardExpires: 4_000, + apiCacheSoftExpires: 2_000, + }); + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache'); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still'); + + const record = store.createRecord<{ identifier: StableRecordIdentifier; [Type]: 'test' }>('test', {}); + + await store.request({ + url: '/test', + method: 'POST', + op: 'createRecord', + records: [record.identifier], + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + }); + + test('@ember-data/request-utils legacy createRecord operations invalidate the CachePolicy type list', async function (assert) { + class AppAdapter { + createRecord( + _store: Store, + _type: unknown, + _snapshot: Snapshot + ): Promise<{ data: { id: string; type: string } }> { + assert.step('adapter:createRecord'); + return Promise.resolve({ data: { id: '1', type: 'test' } }); + } + } + const adapter = new AppAdapter(); + class InterceptLifetimes extends CachePolicy { + override didRequest( + request: ImmutableRequestInfo, + response: Response | ResponseInfo | null, + identifier: StableDocumentIdentifier | null, + store: Store + ): void { + assert.step('didRequest'); + super.didRequest(request, response, identifier, store); + } + override isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isHardExpired(identifier, store); + assert.step(`isHardExpired: ${result}`); + return result; + } + override isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isSoftExpired(identifier, store); + assert.step(`isSoftExpired: ${result}`); + if (result) { + // debugger; + super.isSoftExpired(identifier, store); + } + return result; + } + } + const handleIntercept = { + request(context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + const response = new Response(); + response.headers.set('date', new Date().toUTCString()); + context.setResponse(response); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([LegacyNetworkHandler, handleIntercept]); + this.lifetimes = new InterceptLifetimes({ + apiCacheHardExpires: 4_000, + apiCacheSoftExpires: 2_000, + }); + } + adapterFor() { + return adapter; + } + serializerFor() { + return null; + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache'); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still'); + + const record = store.createRecord('test', {}); + await store.saveRecord(record); + + assert.verifySteps(['adapter:createRecord', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + }); + + test('An AdHoc createRecord request can invalidate the request cache via records', async function (assert) { + class InterceptLifetimes extends CachePolicy { + override didRequest( + request: ImmutableRequestInfo, + response: Response | ResponseInfo | null, + identifier: StableDocumentIdentifier | null, + store: Store + ): void { + assert.step('didRequest'); + super.didRequest(request, response, identifier, store); + } + override isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isHardExpired(identifier, store); + assert.step(`isHardExpired: ${result}`); + return result; + } + override isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isSoftExpired(identifier, store); + assert.step(`isSoftExpired: ${result}`); + if (result) { + // debugger; + super.isSoftExpired(identifier, store); + } + return result; + } + } + const handleIntercept = { + request(context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + const response = new Response(); + response.headers.set('date', new Date().toUTCString()); + context.setResponse(response); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = new InterceptLifetimes({ + apiCacheHardExpires: 4_000, + apiCacheSoftExpires: 2_000, + }); + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache'); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still'); + + // issue an out of band createRecord request with a record identifier + const record = store.createRecord<{ identifier: StableRecordIdentifier; [Type]: 'test' }>('test', {}); + await store.requestManager.request({ + store, + url: '/test', + method: 'POST', + op: 'createRecord', + records: [record.identifier], + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + }); + + test('An AdHoc createRecord request can invalidate the request cache via cacheOptions', async function (assert) { + class InterceptLifetimes extends CachePolicy { + override didRequest( + request: ImmutableRequestInfo, + response: Response | ResponseInfo | null, + identifier: StableDocumentIdentifier | null, + store: Store + ): void { + assert.step('didRequest'); + super.didRequest(request, response, identifier, store); + } + override isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isHardExpired(identifier, store); + assert.step(`isHardExpired: ${result}`); + return result; + } + override isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean { + const result = super.isSoftExpired(identifier, store); + assert.step(`isSoftExpired: ${result}`); + if (result) { + // debugger; + super.isSoftExpired(identifier, store); + } + return result; + } + } + const handleIntercept = { + request(context: RequestContext, _next: NextFn): Promise { + assert.step('request issued'); + const response = new Response(); + response.headers.set('date', new Date().toUTCString()); + context.setResponse(response); + return Promise.resolve({ data: { id: '1', type: 'test' } }) as Promise; + }, + }; + class TestStore extends BaseTestStore { + constructor() { + super(); + this.requestManager = new RequestManager(); + this.requestManager.useCache(CacheHandler); + this.requestManager.use([handleIntercept]); + this.lifetimes = new InterceptLifetimes({ + apiCacheHardExpires: 4_000, + apiCacheSoftExpires: 2_000, + }); + } + } + + const store = new TestStore(); + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache'); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still'); + + // create an out of band createRecord request with no associated identifier + // but with cacheOptions + await store.requestManager.request({ + store, + cacheOptions: { + types: ['test'], + }, + url: '/test', + method: 'POST', + op: 'createRecord', + }); + + assert.verifySteps(['request issued', 'didRequest'], 'we issue the request since it is a different request'); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/1', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: true', 'request issued', 'didRequest'], + 'we are hard expired due to the createRecord response' + ); + + await store.request({ + url: '/test/2', + method: 'GET', + op: 'query', + cacheOptions: { + types: ['test'], + }, + }); + + assert.verifySteps( + ['isHardExpired: false', 'isSoftExpired: false'], + 'we are no longer hard expired due to the createRecord response' + ); + }); +}); diff --git a/tests/main/tests/integration/cache-handler/request-dedupe-test.ts b/tests/main/tests/integration/cache-handler/request-dedupe-test.ts new file mode 100644 index 00000000000..08f5d6bae1e --- /dev/null +++ b/tests/main/tests/integration/cache-handler/request-dedupe-test.ts @@ -0,0 +1,361 @@ +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; +import type { Handler, RequestContext } from '@ember-data/request'; +import RequestManager from '@ember-data/request'; +import { CacheHandler } from '@ember-data/store'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; +import type { Type } from '@warp-drive/core-types/symbols'; + +class User extends Model { + @attr declare name: string; + declare [Type]: 'user'; +} + +module('Integration | Cache Handler | Request Dedupe', function (hooks) { + setupTest(hooks); + + test('it dedupes requests', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; + const TestHandler: Handler = { + request(context: RequestContext) { + assert.step(`requested: ${context.request.url}`); + return Promise.resolve({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'runspired', + }, + }, + } as T); + }, + }; + store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); + + // trigger simultaneous requests + const req1 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req2 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req3 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + + // wait for all requests to resolve + const [res1, res2, res3] = await Promise.all([req1, req2, req3]); + + // assert that all requests were deduped + assert.strictEqual(res1.content.data?.name, 'runspired', 'first request resolved correctly'); + assert.strictEqual(res2.content.data?.name, 'runspired', 'second request resolved correctly'); + assert.strictEqual(res3.content.data?.name, 'runspired', 'third request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // ensure subsequent requests are not deduped + const res4 = await store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + assert.strictEqual(res4.content.data?.name, 'runspired', 'fourth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + }); + + test('it dedupes requests when backgroundReload is used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; + const TestHandler: Handler = { + request(context: RequestContext) { + assert.step(`requested: ${context.request.url}`); + return Promise.resolve({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'runspired', + }, + }, + } as T); + }, + }; + store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); + + // trigger simultaneous requests + const req1 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req2 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + const req3 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + + // wait for all requests to resolve + const [res1, res2, res3] = await Promise.all([req1, req2, req3]); + + // assert that all requests were deduped + assert.strictEqual(res1.content.data?.name, 'runspired', 'first request resolved correctly'); + assert.strictEqual(res2.content.data?.name, 'runspired', 'second request resolved correctly'); + assert.strictEqual(res3.content.data?.name, 'runspired', 'third request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // ensure subsequent requests are not deduped + const res4 = await store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + assert.strictEqual(res4.content.data?.name, 'runspired', 'fourth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + }); + + test('it dedupes requests when reload is used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; + const TestHandler: Handler = { + request(context: RequestContext) { + assert.step(`requested: ${context.request.url}`); + return Promise.resolve({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'runspired', + }, + }, + } as T); + }, + }; + store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); + + // trigger simultaneous requests + const req1 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req2 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + const req3 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + + // wait for all requests to resolve + const [res1, res2, res3] = await Promise.all([req1, req2, req3]); + + // assert that all requests were deduped + assert.strictEqual(res1.content.data?.name, 'runspired', 'first request resolved correctly'); + assert.strictEqual(res2.content.data?.name, 'runspired', 'second request resolved correctly'); + assert.strictEqual(res3.content.data?.name, 'runspired', 'third request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // ensure subsequent requests are not deduped + const res4 = await store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + assert.strictEqual(res4.content.data?.name, 'runspired', 'fourth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + }); + + test('it dedupes requests when backgroundReload and reload are used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; + const TestHandler: Handler = { + request(context: RequestContext) { + assert.step(`requested: ${context.request.url}`); + return Promise.resolve({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'runspired', + }, + }, + } as T); + }, + }; + store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); + + // trigger simultaneous requests + const req1 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req2 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + const req3 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + + // wait for all requests to resolve + const [res1, res2, res3] = await Promise.all([req1, req2, req3]); + + // assert that all requests were deduped + assert.strictEqual(res1.content.data?.name, 'runspired', 'first request resolved correctly'); + assert.strictEqual(res2.content.data?.name, 'runspired', 'second request resolved correctly'); + assert.strictEqual(res3.content.data?.name, 'runspired', 'third request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // ensure subsequent requests are not deduped + const res4 = await store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + assert.strictEqual(res4.content.data?.name, 'runspired', 'fourth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + }); + + test('it dedupes requests when backgroundReload and reload are used (multi-round)', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; + let totalRequests = 0; + const TestHandler: Handler = { + async request(context: RequestContext) { + assert.step(`requested: ${context.request.url}`); + await new Promise((r) => setTimeout(r, 1)); + return Promise.resolve({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'runspired' + ++totalRequests, + }, + }, + } as T); + }, + }; + store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); + + // trigger simultaneous requests + const req1 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req2 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + const req3 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + // wait for all requests to resolve + const [res1, res2, res3] = await Promise.all([req1, req2, req3]); + + // assert that all requests were deduped + assert.strictEqual(res1.content.data?.name, 'runspired1', 'first request resolved correctly'); + assert.strictEqual(res2.content.data?.name, 'runspired1', 'second request resolved correctly'); + assert.strictEqual(res3.content.data?.name, 'runspired1', 'third request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // round 2 + // trigger simultaneous requests + const req4 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req5 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + const req6 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + const res4 = await req4; + assert.strictEqual(res4.content.data?.name, 'runspired1', 'fourth request resolved correctly from cache'); + + const res5 = await req5; + assert.strictEqual(res4.content.data?.name, 'runspired1', 'fifth request resolved correctly from cache'); + + const res6 = await req6; + + // assert that all requests were deduped + assert.strictEqual(res4.content.data?.name, 'runspired2', 'fourth request resolved correctly'); + assert.strictEqual(res5.content.data?.name, 'runspired2', 'fifth request resolved correctly'); + assert.strictEqual(res6.content.data?.name, 'runspired2', 'sixth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // round 3 + // trigger simultaneous requests + const req7 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + const req8 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req9 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + + const res7 = await req7; + assert.strictEqual(res7.content.data?.name, 'runspired2', 'seventh request resolved correctly from cache'); + + const res8 = await req8; + assert.strictEqual(res8.content.data?.name, 'runspired2', 'eigth request resolved correctly from cache'); + + const res9 = await req9; + + // assert that all requests were deduped + assert.strictEqual(res7.content.data?.name, 'runspired3', 'seventh request resolved correctly'); + assert.strictEqual(res8.content.data?.name, 'runspired3', 'eigth request resolved correctly'); + assert.strictEqual(res9.content.data?.name, 'runspired3', 'ninth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + + // round 4 + // trigger simultaneous requests + const req10 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { reload: true }, + }); + const req11 = store.request>({ url: '/users/1', op: 'query', method: 'GET' }); + const req12 = store.request>({ + url: '/users/1', + op: 'query', + method: 'GET', + cacheOptions: { backgroundReload: true }, + }); + + const res11 = await req11; + assert.strictEqual(res11.content.data?.name, 'runspired3', 'eleventh request resolved correctly from cache'); + + const res12 = await req12; + assert.strictEqual(res12.content.data?.name, 'runspired3', 'twelfth request resolved correctly from cache'); + + const res10 = await req10; + + // assert that all requests were deduped + assert.strictEqual(res10.content.data?.name, 'runspired4', 'tenth request resolved correctly'); + assert.strictEqual(res11.content.data?.name, 'runspired4', 'eleventh request resolved correctly'); + assert.strictEqual(res12.content.data?.name, 'runspired4', 'twelfth request resolved correctly'); + assert.verifySteps(['requested: /users/1'], 'only one request was made'); + }); +}); diff --git a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts index f72392ddc31..a6f16ef951a 100644 --- a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts +++ b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts @@ -6,32 +6,38 @@ import { setupTest } from 'ember-qunit'; import Cache from '@ember-data/json-api'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; +import type { Future, NextFn, StructuredDataDocument, StructuredErrorDocument } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { - Future, - NextFn, - StructuredDataDocument, - StructuredErrorDocument, -} from '@ember-data/request/-private/types'; import Fetch from '@ember-data/request/fetch'; +import type { Document, NotificationType } from '@ember-data/store'; import Store, { CacheHandler, recordIdentifierFor } from '@ember-data/store'; -import type { Document } from '@ember-data/store/-private/document'; -import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import { Collection } from '@ember-data/store/-private/record-arrays/identifier-array'; +import type { CollectionRecordArray } from '@ember-data/store/-private'; +import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; +import type { + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { OpaqueRecordInstance } from '@warp-drive/core-types/record'; +import type { RequestContext } from '@warp-drive/core-types/request'; +import type { HashFn } from '@warp-drive/core-types/schema/concepts'; +import type { FieldSchema, HashField } from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument, ResourceDataDocument, SingleResourceDataDocument, -} from '@ember-data/types/cache/document'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; +} from '@warp-drive/core-types/spec/document'; +import type { ExistingResourceObject, ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; +import type { Type } from '@warp-drive/core-types/symbols'; type FakeRecord = { [key: string]: unknown; destroy: () => void }; +type UserRecord = { + id: string; + name: string; + identifier: StableRecordIdentifier; + destroy: () => void; + [Type]: 'user'; +}; class RequestManagerService extends RequestManager { constructor() { @@ -44,16 +50,62 @@ class RequestManagerService extends RequestManager { class TestStore extends Store { @service('request') declare requestManager: RequestManager; - createCache(wrapper: CacheStoreWrapper) { + createSchemaService(): SchemaService { + const schemaService: SchemaService = { + registerDerivation() { + throw new Error('Method not implemented.'); + }, + registerTransformation() { + throw new Error('Method not implemented.'); + }, + registerResources() { + throw new Error('Method not implemented.'); + }, + registerResource() { + throw new Error('Method not implemented.'); + }, + resource() { + throw new Error('Method not implemented.'); + }, + transformation() { + throw new Error('Method not implemented.'); + }, + derivation() { + throw new Error('Method not implemented.'); + }, + fields(identifier: StableRecordIdentifier | { type: string }): Map { + return new Map(); + }, + hasTrait() { + return false; + }, + resourceHasTrait() { + return false; + }, + hasResource() { + return true; + }, + registerHashFn: function (hashFn: HashFn): void { + throw new Error('Function not implemented.'); + }, + hashFn: function (field: HashField | { type: string }): HashFn { + throw new Error('Function not implemented.'); + }, + }; + + return schemaService; + } + + override createCache(wrapper: CacheCapabilitiesManager) { return new Cache(wrapper); } - instantiateRecord(identifier: StableRecordIdentifier) { + override instantiateRecord(identifier: StableRecordIdentifier) { const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); + const record: FakeRecord = { id, lid, type, identifier } as unknown as FakeRecord; + Object.assign(record, this.cache.peek(identifier)!.attributes); - let token = this.notifications.subscribe( + const token = this.notifications.subscribe( identifier, (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { if (kind === 'attributes' && key) { @@ -69,7 +121,7 @@ class TestStore extends Store { return record; } - teardownRecord(record: FakeRecord) { + override teardownRecord(record: FakeRecord) { record.destroy(); } } @@ -102,12 +154,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching a resource document loads the cache and hydrates the record', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; - const userDocument = await store.request>({ + const store = owner.lookup('service:store') as unknown as TestStore; + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); @@ -134,7 +186,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('re-fetching a resource document returns from cache as expected', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -167,11 +219,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); @@ -195,7 +247,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/1.json', }); const data2 = userDocument2.content.data; @@ -225,7 +277,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching a resource document that errors', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; try { await store.request({ url: '/assets/users/2.json', @@ -242,57 +294,16 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('When using @ember-data/store, the cache-handler can hydrate any op code', async function (assert) { const { owner } = this; - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } - - instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); - - let token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); - const store = owner.lookup('service:store') as TestStore; - const userDocument = await store.request>({ + const store = owner.lookup('service:store') as unknown as TestStore; + const userDocument = await store.request>({ op: 'random-op', url: '/assets/users/1.json', }); - const identifier = recordIdentifierFor(userDocument.content.data!); - const record = store.peekRecord(identifier); + const identifier = recordIdentifierFor(userDocument.content.data); + const record = store.peekRecord(identifier); assert.strictEqual(record?.name, 'Chris Thoburn'); assert.strictEqual(userDocument.content.data, record, 'we get a hydrated record back as data'); @@ -320,51 +331,10 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('When using @ember-data/store, the cache-handler will cache but not hydrate if the request has the store but does not originate from the store', async function (assert) { const { owner } = this; - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } - - instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); - - let token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; const userDocument = await store.requestManager.request({ store, url: '/assets/users/1.json', @@ -392,59 +362,18 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const record = store.peekRecord(userDocument.content.data!); + const record = store.peekRecord(userDocument.content.data!); assert.strictEqual(record?.name, 'Chris Thoburn'); }); test('When using @ember-data/store, the cache-handler will neither cache nor hydrate if the request does not originate from the store and no store is included', async function (assert) { const { owner } = this; - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - createCache(wrapper: CacheStoreWrapper) { - return new Cache(wrapper); - } - - instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, (this.cache.peek(identifier) as JsonApiResource).attributes); - - let token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); - const store = owner.lookup('service:store') as TestStore; - const userDocument = await store.requestManager.request>({ + const store = owner.lookup('service:store') as unknown as TestStore; + const userDocument = await store.requestManager.request>({ url: '/assets/users/1.json', }); @@ -482,7 +411,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('background re-fetching a resource returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -531,12 +460,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); - const data = userDocument.content.data!; + const record = store.peekRecord(identifier); + const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', ' record name is correct'); assert.strictEqual(data, record, ' record was returned as data'); @@ -560,11 +489,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ' we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/1.json', cacheOptions: { backgroundReload: true }, }); - const data2 = userDocument2.content.data!; + const data2 = userDocument2.content.data; assert.strictEqual(data2, record, ' record was returned as data'); assert.strictEqual(data2 && recordIdentifierFor(data2), identifier, ' we get a record back as data'); @@ -589,9 +518,9 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { await store._getAllPending(); - const data3 = userDocument2.content.data!; + const data3 = userDocument2.content.data; const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const record2 = store.peekRecord(identifier2); + const record2 = store.peekRecord(identifier2); assert.strictEqual(record2?.name, 'Wesley Thoburn', ' record2 name is correct'); assert.strictEqual(userDocument.content, userDocument2.content, ' documents are the same'); @@ -623,7 +552,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching with hydration, then background re-fetching a resource without hydration returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -672,12 +601,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); - const data = userDocument.content.data!; + const record = store.peekRecord(identifier); + const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', ' record name is correct'); assert.strictEqual(data, record, ' record was returned as data'); @@ -701,7 +630,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ' we get access to the document meta' ); - // Backgrond Re-Fetch without Hydration + // Background Re-Fetch without Hydration const userDocument2 = await store.requestManager.request({ store, url: '/assets/users/1.json', @@ -734,11 +663,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { // Assert the initial document was updated const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const record2 = store.peekRecord(identifier2); + const record2 = store.peekRecord(identifier2); assert.strictEqual(handlerCalls, 2, 'fetch handler should only be called twice'); assert.strictEqual(record2?.name, 'Wesley Thoburn', 'record2 name is correct'); - const data3 = userDocument.content.data!; + const data3 = userDocument.content.data; assert.strictEqual(record2?.name, 'Wesley Thoburn', ' record2 name is correct'); assert.strictEqual(userDocument.content, userDocument.content, ' documents are the same'); @@ -769,7 +698,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('background re-fetching a resource without hydration returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -823,8 +752,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store, url: '/assets/users/1.json', }); - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + const record = store.peekRecord(identifier); const data = userDocument.content.data!; assert.strictEqual(record?.name, 'Chris Thoburn', ' record name is correct'); @@ -882,7 +814,10 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.identifierCache.getOrCreateDocumentIdentifier({ url: '/assets/users/1.json' })! ) as unknown as StructuredDataDocument; const data3 = updatedUserDocument?.content?.data; - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '2', + }) as StableExistingRecordIdentifier; assert.strictEqual(data3, identifier2, 'we get an identifier back as data'); assert.strictEqual(updatedUserDocument.content.lid, '/assets/users/1.json', 'we get back url as the cache key'); @@ -909,7 +844,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { module('Collection', function () { test('re-fetching a resource collection returns from cache as expected', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -945,11 +880,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); const data = userDocument.content.data!; assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); @@ -976,7 +911,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/list.json', }); const data2 = userDocument2.content.data!; @@ -1008,7 +943,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('background re-fetching a resource collection returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -1059,11 +994,11 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); const data = userDocument.content.data!; assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); @@ -1090,7 +1025,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/list.json', cacheOptions: { backgroundReload: true }, }); @@ -1122,7 +1057,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { await store._getAllPending(); const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const record2 = store.peekRecord(identifier2); + const record2 = store.peekRecord(identifier2); assert.strictEqual(record2?.name, 'Wesley Thoburn', 'record2 name is correct'); assert.strictEqual(data.length, 2, 'recordArray has two records'); @@ -1156,7 +1091,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching with hydration, then background re-fetching a resource collection without hydration returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -1208,12 +1143,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.requestManager.useCache(CacheHandler); // Initial Fetch with Hydration - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const data = userDocument.content.data!; - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); assert.true(Array.isArray(data), 'recordArray was returned as data'); @@ -1239,7 +1174,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - // Backgrond Re-Fetch without Hydration + // Background Re-Fetch without Hydration const userDocument2 = await store.requestManager.request({ store, url: '/assets/users/list.json', @@ -1275,7 +1210,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { // Assert the initial document was updated const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); - const record2 = store.peekRecord(identifier2); + const record2 = store.peekRecord(identifier2); assert.strictEqual(handlerCalls, 2, 'fetch handler should only be called twice'); assert.strictEqual(record2?.name, 'Wesley Thoburn', 'record2 name is correct'); @@ -1317,7 +1252,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('background re-fetching a resource collection without hydration returns from cache as expected, updates once complete', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); @@ -1463,17 +1398,194 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { }); }); + module('Mutation', function () { + test('when an updateRecord results in a 204 we do not error', async function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as unknown as TestStore; + + store.requestManager = new RequestManager(); + store.requestManager.use([ + { + request(context: RequestContext) { + assert.step('request'); + + context.setResponse( + new Response(null, { + status: 204, + statusText: 'No Content', + }) + ); + + return Promise.resolve(null) as Promise; + }, + }, + ]); + store.requestManager.useCache(CacheHandler); + + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris Thoburn' } } }); + assert.false(store.cache.hasChangedAttrs(record.identifier), 'record is clean'); + store.cache.setAttr(record.identifier, 'name', 'Wesley Thoburn'); + assert.true(store.cache.hasChangedAttrs(record.identifier), 'record is dirty'); + await store.request({ + op: 'updateRecord', + method: 'PATCH', + url: '/users', + records: [record.identifier], + }); + assert.false(store.cache.hasChangedAttrs(record.identifier), 'record is clean'); + assert.verifySteps(['request']); + }); + + test('when a createRecord results in a 201 we do not error so long as we already had an ID', async function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as unknown as TestStore; + + store.requestManager = new RequestManager(); + store.requestManager.use([ + { + request(context: RequestContext) { + assert.step('request'); + + context.setResponse( + new Response('{}', { + status: 201, + statusText: 'Created', + }) + ); + + return Promise.resolve({}) as Promise; + }, + }, + ]); + store.requestManager.useCache(CacheHandler); + + const record = store.createRecord('user', { id: '1', name: 'Chris Thoburn' }); + assert.true(store.cache.isNew(record.identifier), 'record is new'); + await store.request({ + op: 'createRecord', + method: 'POST', + url: '/users', + records: [record.identifier], + }); + assert.false(store.cache.isNew(record.identifier), 'record is saved'); + assert.verifySteps(['request']); + }); + + test('when a createRecord results in a 204 we do not error so long as we already had an ID', async function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as unknown as TestStore; + + store.requestManager = new RequestManager(); + store.requestManager.use([ + { + request(context: RequestContext) { + assert.step('request'); + + context.setResponse( + new Response(null, { + status: 204, + statusText: 'No Content', + }) + ); + + return Promise.resolve(null) as Promise; + }, + }, + ]); + store.requestManager.useCache(CacheHandler); + + const record = store.createRecord('user', { id: '1', name: 'Chris Thoburn' }); + assert.true(store.cache.isNew(record.identifier), 'record is new'); + await store.request({ + op: 'createRecord', + method: 'POST', + url: '/users', + records: [record.identifier], + }); + assert.false(store.cache.isNew(record.identifier), 'record is saved'); + assert.verifySteps(['request']); + }); + + test('when a createRecord results in a 201 and had no records, we do not error', async function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as unknown as TestStore; + + store.requestManager = new RequestManager(); + store.requestManager.use([ + { + request(context: RequestContext) { + assert.step('request'); + + context.setResponse( + new Response(null, { + status: 204, + statusText: 'No Content', + }) + ); + + return Promise.resolve(null) as Promise; + }, + }, + ]); + store.requestManager.useCache(CacheHandler); + + await store.requestManager.request({ + store, // use the CacheHandler but don't hydrate + op: 'createRecord', + method: 'POST', + url: '/users', + }); + assert.verifySteps(['request']); + }); + + test('when a createRecord results in a 204 and had no records, we do not error', async function (assert) { + const { owner } = this; + + const store = owner.lookup('service:store') as unknown as TestStore; + + store.requestManager = new RequestManager(); + store.requestManager.use([ + { + request(context: RequestContext) { + assert.step('request'); + + context.setResponse( + new Response(null, { + status: 204, + statusText: 'No Content', + }) + ); + + return Promise.resolve(null) as Promise; + }, + }, + ]); + store.requestManager.useCache(CacheHandler); + await store.requestManager.request({ + store, // use the CacheHandler but don't hydrate + op: 'createRecord', + method: 'POST', + url: '/users', + }); + assert.verifySteps(['request']); + }); + }); + module('Errors', function () { test('fetching a resource document that errors, request can be replayed', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; store.requestManager = new RequestManager(); store.requestManager.use([ LegacyNetworkHandler, { - request(context: Context, next: NextFn): Future { + request(context: RequestContext, next: NextFn): Future { if (handlerCalls > 0) { assert.ok(false, 'fetch handler should not be called again'); } @@ -1487,20 +1599,23 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { const docIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ url: '/assets/users/2.json' })!; try { - await store.request({ + await store.request({ url: '/assets/users/2.json', }); assert.ok(false, 'we should error'); } catch (errorDocument: unknown) { assertIsErrorDocument(assert, errorDocument); - assert.true(errorDocument.message.startsWith('[404 Not Found] GET (basic) - '), 'We receive the correct error'); + assert.true( + errorDocument.message.startsWith('[404 Not Found] GET (basic) - '), + `We receive the correct error: ${errorDocument.message}` + ); } assert.strictEqual(handlerCalls, 1, 'fetch handler should be called once'); const doc = store.cache.peekRequest(docIdentifier) as unknown as StructuredErrorDocument; try { - await store.request({ + await store.request({ url: '/assets/users/2.json', }); assert.ok(false, 'we should error'); @@ -1521,7 +1636,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching a resource document that errors with detail, errors available as content', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; function getErrorPayload(lid?: string | StableDocumentIdentifier) { if (lid) { @@ -1584,7 +1699,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { })!; try { - await store.request({ + await store.request({ url: '/assets/users/2.json?include=author', }); assert.ok(false, 'we should error'); @@ -1602,7 +1717,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { const doc = store.cache.peekRequest(docIdentifier) as unknown as StructuredErrorDocument; try { - await store.request({ + await store.request({ url: '/assets/users/2.json?include=author', }); assert.ok(false, 'we should error'); @@ -1629,7 +1744,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('fetching a resource document that succeeds, then later errors with detail, errors available as content', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; function getErrorPayload(lid?: string | StableDocumentIdentifier) { if (lid) { @@ -1703,7 +1818,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { const resourceIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); // Initial successful fetch - const originalDoc = await store.request>({ + const originalDoc = await store.request>({ url: '/assets/users/2.json?include=author', }); const originalRawDoc = store.cache.peekRequest( @@ -1715,7 +1830,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { // First failed fetch try { - await store.request({ + await store.request({ url: '/assets/users/2.json?include=author', cacheOptions: { reload: true }, }); @@ -1738,7 +1853,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { // Replay of failed fetch try { - await store.request({ + await store.request({ url: '/assets/users/2.json?include=author', }); assert.ok(false, ' we should error'); @@ -1795,7 +1910,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { module('AbortController', function () { test('aborting a request pre-cache-insert does not affect the cache', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; const resourceIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ url: '/assets/users/list.json', @@ -1811,7 +1926,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.requestManager.use([ LegacyNetworkHandler, { - async request(_request: Context, _nextFn: NextFn): Promise { + async request(_request: RequestContext, _nextFn: NextFn): Promise { handlerCalls++; resolve(); await next; @@ -1851,7 +1966,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('aborting a request post-cache-insert maintains cache-update but returns abort rejection', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; const resourceIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ url: '/assets/users/list.json', @@ -1867,7 +1982,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.requestManager.use([ LegacyNetworkHandler, { - async request(_request: Context, _nextFn: NextFn): Promise { + async request(_request: RequestContext, _nextFn: NextFn): Promise { handlerCalls++; return Promise.resolve({ data: { @@ -1880,7 +1995,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { }, ]); store.requestManager.useCache({ - async request(context: Context, next: NextFn): Promise { + async request(context: RequestContext, next: NextFn): Promise { const cacheComplete = await CacheHandler.request(context, next); resolve(); await nextPromise; @@ -1913,15 +2028,15 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('aborting a request post-request does nothing', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; - const request = store.request>({ + const store = owner.lookup('service:store') as unknown as TestStore; + const request = store.request>({ url: '/assets/users/1.json', }); const userDocument = await request; request.abort(); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); + const record = store.peekRecord(identifier); const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', 'record name is correct'); @@ -1948,7 +2063,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('aborting a background-request does not result in an uncaught error', async function (assert) { const { owner } = this; - const store = owner.lookup('service:store') as TestStore; + const store = owner.lookup('service:store') as unknown as TestStore; let handlerCalls = 0; let resolve!: (v?: unknown) => void; @@ -1957,7 +2072,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.requestManager.use([ LegacyNetworkHandler, { - async request(_context: Context, _next: NextFn): Promise { + async request(_context: RequestContext, _next: NextFn): Promise { if (handlerCalls > 1) { assert.ok(false, 'fetch handler should not be called again'); throw new Error('fetch handler should not be called again'); @@ -2003,12 +2118,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const record = store.peekRecord(identifier); - const data = userDocument.content.data!; + const record = store.peekRecord(identifier); + const data = userDocument.content.data; assert.strictEqual(record?.name, 'Chris Thoburn', ' record name is correct'); assert.strictEqual(data, record, ' record was returned as data'); @@ -2032,12 +2147,12 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ' we get access to the document meta' ); - const request2 = store.request>({ + const request2 = store.request>({ url: '/assets/users/1.json', cacheOptions: { backgroundReload: true }, }); const userDocument2 = await request2; - const data2 = userDocument2.content.data!; + const data2 = userDocument2.content.data; assert.strictEqual(data2, record, ' record was returned as data'); assert.strictEqual(data2 && recordIdentifierFor(data2), identifier, ' we get a record back as data'); @@ -2064,7 +2179,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { resolve(); await store._getAllPending(); - const data3 = userDocument2.content.data!; + const data3 = userDocument2.content.data; const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); const record2 = store.peekRecord(identifier2); diff --git a/tests/main/tests/integration/cache/cache-capabilities-manager-test.ts b/tests/main/tests/integration/cache/cache-capabilities-manager-test.ts new file mode 100644 index 00000000000..e73721b7a53 --- /dev/null +++ b/tests/main/tests/integration/cache/cache-capabilities-manager-test.ts @@ -0,0 +1,167 @@ +import { settled } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; + +class Person extends Model { + @attr('string', {}) + name; +} + +class Car extends Model { + @belongsTo('house', { async: true, inverse: 'car' }) + garage; + + @attr('string', {}) + make; +} + +class House extends Model { + @attr('string', {}) + name; + + @belongsTo('person', { async: false, inverse: null }) + landlord; + + @belongsTo('car', { async: false, inverse: 'garage' }) + car; + + @hasMany('person', { async: false, inverse: null }) + tenants; +} + +module('integration/cache-capabilities', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + + owner.register('model:person', Person); + owner.register('model:house', House); + owner.register('model:car', Car); + }); + + test('schema', function (assert) { + const { owner } = this; + let capabilities!: CacheCapabilitiesManager; + + class TestStore extends Store { + override createCache(cacheCapabilities: CacheCapabilitiesManager) { + capabilities = cacheCapabilities; + return super.createCache(cacheCapabilities); + } + } + + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as unknown as Store; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + store.cache; + + assert.strictEqual(capabilities.schema, store.schema, 'capabilities exposes the schema service'); + }); + + test('setRecordId', function (assert) { + const { owner } = this; + let capabilities!: CacheCapabilitiesManager; + + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + capabilities = wrapper; + return super.createCache(wrapper); + } + } + + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as unknown as Store; + + const house = store.createRecord('house', {}) as Model; + capabilities.setRecordId(recordIdentifierFor(house), '17'); + assert.strictEqual(house.id, '17', 'setRecordId correctly set the id'); + assert.strictEqual( + store.peekRecord('house', '17'), + house, + 'can lookup the record from the identify map based on the new id' + ); + }); + + test('hasRecord', function (assert) { + const { owner } = this; + + let storeWrapper!: CacheCapabilitiesManager; + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + storeWrapper = wrapper; + return super.createCache(wrapper); + } + } + + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as unknown as Store; + + store.push({ + data: [ + { + type: 'house', + id: '1', + attributes: { + name: 'Moomin', + }, + }, + + { + type: 'house', + id: '2', + attributes: { + name: 'Lodge', + }, + }, + ], + }); + store.peekRecord('house', '1'); + + // TODO isRecordInUse returns true if record has never been instantiated, think through whether thats correct + const house2 = store.peekRecord('house', '2') as Model; + house2.unloadRecord(); + + store.createRecord('house', {}); + const id1 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '1' }); + const id2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); + assert.true(storeWrapper.hasRecord(id1), 'house 1 is in use'); + assert.false(storeWrapper.hasRecord(id2), 'house 2 is not in use'); + }); + + test('disconnectRecord', async function (assert) { + const { owner } = this; + + let storeWrapper!: CacheCapabilitiesManager; + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + storeWrapper = wrapper; + return super.createCache(wrapper); + } + } + + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as unknown as Store; + + const identifier = store._push({ + data: { + type: 'house', + id: '1', + attributes: { + name: 'Moomin', + }, + }, + }); + storeWrapper.disconnectRecord(identifier as StableRecordIdentifier); + await settled(); + assert.strictEqual(store.peekRecord('house', '1'), null, 'record was removed from id map'); + }); +}); diff --git a/tests/main/tests/integration/cache/json-api-cache-test.js b/tests/main/tests/integration/cache/json-api-cache-test.js index b3bc5e09627..d3d19d0219f 100644 --- a/tests/main/tests/integration/cache/json-api-cache-test.js +++ b/tests/main/tests/integration/cache/json-api-cache-test.js @@ -4,7 +4,6 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; import Model, { attr } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; @@ -33,7 +32,7 @@ module('@ember-data/json-api | Cache', function (hooks) { const user = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Wesley Youman' } } }); const identifier = recordIdentifierFor(user); user.name = 'Wesley Thoburn'; - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const cache = store.cache; assert.true(user.hasDirtyAttributes, 'the record is dirty before save'); assert.true(cache.hasChangedAttrs(identifier), 'the cache reflects changes before save'); diff --git a/tests/main/tests/integration/cache/spec-cache-errors-test.ts b/tests/main/tests/integration/cache/spec-cache-errors-test.ts new file mode 100644 index 00000000000..2bf29f0871c --- /dev/null +++ b/tests/main/tests/integration/cache/spec-cache-errors-test.ts @@ -0,0 +1,389 @@ +import EmberObject from '@ember/object'; + +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import { InvalidError } from '@ember-data/adapter/error'; +import Model, { attr } from '@ember-data/model'; +import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases'; +import type { Change } from '@warp-drive/core-types/cache/change'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { + RecordIdentifier, + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { + CollectionResourceDataDocument, + ResourceDocument, + ResourceErrorDocument, + ResourceMetaDocument, + SingleResourceDataDocument, +} from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { + CollectionResourceDocument, + ExistingResourceObject, + JsonApiDocument, + SingleResourceDocument, +} from '@warp-drive/core-types/spec/json-api-raw'; + +class Person extends Model { + @attr declare firstName: string; + @attr declare lastName: string; +} + +class TestCache implements Cache { + wrapper: CacheCapabilitiesManager; + _data: Map = new Map(); + constructor(wrapper: CacheCapabilitiesManager) { + this.wrapper = wrapper; + } + changedRelationships(identifier: StableRecordIdentifier): Map { + throw new Error('Method not implemented.'); + } + hasChangedRelationships(identifier: StableRecordIdentifier): boolean { + throw new Error('Method not implemented.'); + } + rollbackRelationships(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + patch(op: MergeOperation): void { + throw new Error('Method not implemented.'); + } + put(doc: StructuredDocument): SingleResourceDataDocument; + put(doc: StructuredDocument): CollectionResourceDataDocument; + put( + doc: StructuredDocument + ): ResourceMetaDocument | ResourceErrorDocument; + put(doc: StructuredDocument): ResourceDocument { + if ('content' in doc && !('error' in doc)) { + const identifier = this.wrapper.identifierCache.getOrCreateRecordIdentifier(doc.content.data as RecordIdentifier); + this.upsert(identifier, doc.content.data as ExistingResourceObject, this.wrapper.hasRecord(identifier)); + return { data: identifier } as SingleResourceDataDocument; + } else if ('error' in doc) { + throw typeof doc.error === 'string' ? new Error(doc.error) : (doc.error as Error); + } + throw new Error('Not Implemented'); + } + + peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { + throw new Error(`Not Implemented`); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + throw new Error(`Not Implemented`); + } + fork(): Promise { + throw new Error(`Not Implemented`); + } + merge(cache: Cache): Promise { + throw new Error(`Not Implemented`); + } + diff(): Promise { + throw new Error(`Not Implemented`); + } + dump(): Promise> { + throw new Error(`Not Implemented`); + } + hydrate(stream: ReadableStream): Promise { + throw new Error('Not Implemented'); + } + + mutate(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } + version = '2' as const; + + _errors?: ApiError[]; + _isNew = false; + + upsert( + identifier: StableRecordIdentifier, + data: ExistingResourceObject, + calculateChanges?: boolean + ): void | string[] { + if (!this._data.has(identifier)) { + this.wrapper.notifyChange(identifier, 'added'); + } + this._data.set(identifier, data); + this.wrapper.notifyChange(identifier, 'attributes'); + this.wrapper.notifyChange(identifier, 'relationships'); + } + clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { + this._isNew = true; + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit( + identifier: StableRecordIdentifier, + response: StructuredDataDocument + ): SingleResourceDataDocument { + return { data: identifier as StableExistingRecordIdentifier }; + } + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): string { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } + addToHasMany( + identifier: StableRecordIdentifier, + propertyName: string, + value: StableRecordIdentifier[], + idx?: number + ): void { + throw new Error('Method not implemented.'); + } + removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + + getErrors(identifier: StableRecordIdentifier): ApiError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; + } +} + +module('integration/record-data Custom Cache (v2) Errors', function (hooks) { + setupTest(hooks); + + test('Cache Invalid Errors', async function (assert) { + assert.expect(3); + + const { owner } = this; + + class LifecycleCache extends TestCache { + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]) { + super.commitWasRejected(identifier, errors); + assert.strictEqual(errors?.[0]?.detail, 'is a generally unsavoury character', 'received the error'); + assert.strictEqual(errors?.[0]?.source?.pointer, '/data/attributes/name', 'pointer is correct'); + } + } + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + return new LifecycleCache(wrapper) as Cache; + } + } + class TestAdapter extends EmberObject { + updateRecord() { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject( + // @ts-expect-error Constructor of class 'InvalidError' is private + new InvalidError([ + { + title: 'Invalid Attribute', + detail: 'is a generally unsavoury character', + source: { + pointer: '/data/attributes/name', + }, + }, + ]) + ); + } + + createRecord() { + return Promise.resolve(); + } + } + + owner.register('model:person', Person); + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter); + + const store = owner.lookup('service:store') as unknown as Store; + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', + }, + }, + }) as Model; + + try { + await person.save(); + assert.ok(false, 'we should error'); + } catch { + assert.ok(true, 'we erred'); + } + }); + + test('Cache Network Errors', async function (assert) { + assert.expect(2); + + const { owner } = this; + + class LifecycleCache extends TestCache { + override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]) { + super.commitWasRejected(identifier, errors); + assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); + } + } + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + return new LifecycleCache(wrapper) as Cache; + } + } + class TestAdapter extends EmberObject { + updateRecord() { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject(); + } + + createRecord() { + return Promise.resolve(); + } + } + + owner.register('model:person', Person); + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter); + + const store = owner.lookup('service:store') as unknown as Store; + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', + }, + }, + }) as Model; + + try { + await person.save(); + assert.ok(false, 'we should error'); + } catch { + assert.ok(true, 'we erred'); + } + }); + + test('Cache Invalid Errors Can Be Reflected On The Record', function (assert) { + const { owner } = this; + let errorsToReturn: ApiError[] | undefined; + let storeWrapper!: CacheCapabilitiesManager; + + class LifecycleCache extends TestCache { + override getErrors(): ApiError[] { + return errorsToReturn || []; + } + } + + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + storeWrapper = wrapper; + return new LifecycleCache(wrapper) as Cache; + } + } + + owner.register('model:person', Person); + owner.register('service:store', TestStore); + + const store = owner.lookup('service:store') as unknown as Store; + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Tom', + lastName: 'Dale', + }, + }, + }) as Model; + + const identifier = recordIdentifierFor(person); + + let nameError = person.errors.errorsFor('firstName').objectAt(0); + assert.strictEqual(nameError, undefined, 'no error shows up on firstName initially'); + assert.true(person.isValid, 'person is initially valid'); + + errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/firstName', + }, + }, + ]; + storeWrapper.notifyChange(identifier, 'errors'); + + nameError = person.errors.errorsFor('firstName').objectAt(0); + + assert.strictEqual(nameError?.attribute, 'firstName', 'error shows up on name'); + assert.false(person.isValid, 'person is not valid'); + + errorsToReturn = []; + storeWrapper.notifyChange(identifier, 'errors'); + + assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on name'); + assert.true(person.isValid, 'person is valid'); + + errorsToReturn = [ + { + title: 'Invalid Attribute', + detail: '', + source: { + pointer: '/data/attributes/lastName', + }, + }, + ]; + storeWrapper.notifyChange(identifier, 'errors'); + + assert.false(person.isValid, 'person is not valid'); + + assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on firstName'); + + const lastNameError = person.errors.errorsFor('lastName').objectAt(0); + + assert.strictEqual(lastNameError?.attribute, 'lastName', 'error shows up on lastName'); + }); +}); diff --git a/tests/main/tests/integration/cache/spec-cache-state-test.ts b/tests/main/tests/integration/cache/spec-cache-state-test.ts new file mode 100644 index 00000000000..cc83fe7f1f8 --- /dev/null +++ b/tests/main/tests/integration/cache/spec-cache-state-test.ts @@ -0,0 +1,407 @@ +import EmberObject from '@ember/object'; +import { settled } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; +import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases'; +import type { Change } from '@warp-drive/core-types/cache/change'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { + CollectionResourceDataDocument, + ResourceDocument, + ResourceErrorDocument, + ResourceMetaDocument, + SingleResourceDataDocument, +} from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { + CollectionResourceDocument, + ExistingResourceObject, + JsonApiDocument, + SingleResourceDocument, +} from '@warp-drive/core-types/spec/json-api-raw'; + +class Person extends Model { + // TODO fix the typing for naked attrs + + @attr('string', {}) + name; + + @attr('string', {}) + lastName; +} + +class TestCache implements Cache { + _storeWrapper: CacheCapabilitiesManager; + _identifier: StableRecordIdentifier; + + constructor(wrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier) { + this._storeWrapper = wrapper; + this._identifier = identifier; + } + + changedRelationships(identifier: StableRecordIdentifier): Map { + throw new Error('Method not implemented.'); + } + hasChangedRelationships(identifier: StableRecordIdentifier): boolean { + throw new Error('Method not implemented.'); + } + rollbackRelationships(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + + patch(op: MergeOperation): void { + throw new Error('Method not implemented.'); + } + _data: Map = new Map(); + put(doc: StructuredDocument): SingleResourceDataDocument; + put(doc: StructuredDocument): CollectionResourceDataDocument; + put( + doc: StructuredDocument + ): ResourceMetaDocument | ResourceErrorDocument; + put(doc: StructuredDocument): ResourceDocument { + if ('content' in doc && !('error' in doc)) { + if (Array.isArray(doc.content.data)) { + const data = doc.content.data.map((resource) => { + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + resource + ) as StableExistingRecordIdentifier; + this.upsert(identifier, resource, this._storeWrapper.hasRecord(identifier)); + return identifier; + }); + return { data }; + } else { + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + doc.content.data + ) as StableExistingRecordIdentifier; + this.upsert(identifier, doc.content.data!, this._storeWrapper.hasRecord(identifier)); + return { data: identifier } as SingleResourceDataDocument; + } + } else if ('error' in doc) { + throw typeof doc.error === 'string' ? new Error(doc.error) : (doc.error as Error); + } + throw new Error('Not Implemented'); + } + + peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { + throw new Error(`Not Implemented`); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + throw new Error(`Not Implemented`); + } + fork(): Promise { + throw new Error(`Not Implemented`); + } + merge(cache: Cache): Promise { + throw new Error(`Not Implemented`); + } + diff(): Promise { + throw new Error(`Not Implemented`); + } + dump(): Promise> { + throw new Error(`Not Implemented`); + } + hydrate(stream: ReadableStream): Promise { + throw new Error('Not Implemented'); + } + + upsert( + identifier: StableRecordIdentifier, + data: ExistingResourceObject, + calculateChanges?: boolean + ): void | string[] { + if (!this._data.has(identifier)) { + this._storeWrapper.notifyChange(identifier, 'added'); + } + this._data.set(identifier, data); + this._storeWrapper.notifyChange(identifier, 'attributes'); + this._storeWrapper.notifyChange(identifier, 'relationships'); + } + mutate(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } + version = '2' as const; + + _errors?: ApiError[]; + _isNew = false; + + clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { + this._isNew = true; + this._storeWrapper.notifyChange(identifier, 'added'); + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { + return { data: identifier as StableExistingRecordIdentifier }; + } + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): string { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } + addToHasMany( + identifier: StableRecordIdentifier, + propertyName: string, + value: StableRecordIdentifier[], + idx?: number + ): void { + throw new Error('Method not implemented.'); + } + removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + + getErrors(identifier: StableRecordIdentifier): ApiError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; + } +} + +module('integration/record-data - Record Data State', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + + owner.register('model:person', Person); + // @ts-expect-error missing type + owner.unregister('service:store'); + owner.register('service:store', Store); + owner.register('serializer:application', JSONAPISerializer); + }); + + test('Record Data state saving', async function (assert) { + assert.expect(3); + + let isDeleted: boolean, isNew: boolean, isDeletionCommitted: boolean; + let calledDelete = false; + let calledUpdate = false; + let calledCreate = false; + + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }; + const { owner } = this; + + class LifecycleCache extends TestCache { + override isNew(): boolean { + return isNew; + } + + override isDeleted(): boolean { + return isDeleted; + } + + override isDeletionCommitted(): boolean { + return isDeletionCommitted; + } + + override setIsDeleted(): void { + isDeleted = true; + } + } + + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + // @ts-expect-error + return new LifecycleCache(wrapper) as Cache; + } + } + + const TestAdapter = EmberObject.extend({ + deleteRecord() { + calledDelete = true; + return Promise.resolve(); + }, + + updateRecord() { + calledUpdate = true; + return Promise.resolve(); + }, + + createRecord() { + calledCreate = true; + return Promise.resolve(); + }, + }); + + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter, { singleton: false }); + + const store = owner.lookup('service:store') as Store; + + store.push({ + data: [personHash], + }); + + const person = store.peekRecord('person', '1') as Person; + isNew = true; + await person.save(); + assert.true(calledCreate, 'called create if record isNew'); + + isNew = false; + isDeleted = true; + await person.save(); + assert.true(calledDelete, 'called delete if record isDeleted'); + + isNew = false; + isDeleted = false; + + await person.save(); + assert.true(calledUpdate, "called update if record isn't deleted or new"); + }); + + test('Record Data state record flags', async function (assert) { + assert.expect(13); + let isDeleted = false; + let isNew = false; + let isDeletionCommitted = false; + let calledSetIsDeleted = false; + let storeWrapper!: CacheCapabilitiesManager; + + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }; + const { owner } = this; + + class LifecycleCache extends TestCache { + constructor(sw: CacheCapabilitiesManager, identifier: StableRecordIdentifier) { + super(sw, identifier); + storeWrapper = sw; + } + + override isEmpty(): boolean { + return !isNew && isDeletionCommitted; + } + + override isNew(): boolean { + return isNew; + } + + override isDeleted(): boolean { + return isDeleted; + } + + override isDeletionCommitted(): boolean { + return isDeletionCommitted; + } + + override setIsDeleted(identifier: StableRecordIdentifier, value: boolean): void { + isDeleted = true; + calledSetIsDeleted = true; + } + } + + class TestStore extends Store { + override createCache(wrapper: CacheCapabilitiesManager) { + // @ts-expect-error + return new LifecycleCache(wrapper) as Cache; + } + } + + owner.register('service:store', TestStore); + + const store = owner.lookup('service:store') as Store; + + store.push({ + data: [personHash], + }); + + const person = store.peekRecord('person', '1') as Person; + const personIdentifier = recordIdentifierFor(person); + const people = store.peekAll('person'); + assert.strictEqual(people.length, 1, 'live array starting length is 1'); + + isNew = true; + 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'); + await settled(); + assert.false(person.isNew, 'person is not new'); + assert.true(person.isDeleted, 'person is deleted'); + assert.strictEqual(people.length, 1, 'live array starting length is 1'); + + isNew = false; + isDeleted = false; + storeWrapper.notifyChange(personIdentifier, 'state'); + await settled(); + assert.false(person.isNew, 'person is not new'); + assert.false(person.isDeleted, 'person is not deleted'); + assert.strictEqual(people.length, 1, 'live array starting length is 1'); + person.deleteRecord(); + await settled(); + assert.strictEqual(people.length, 1, 'live array starting length is 1 after deleteRecord'); + assert.false(person.isDeleted, 'calling deleteRecord does not automatically set isDeleted flag to true'); + assert.true(calledSetIsDeleted, 'called setIsDeleted'); + + isDeletionCommitted = true; + 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 new file mode 100644 index 00000000000..8ee29368750 --- /dev/null +++ b/tests/main/tests/integration/cache/spec-cache-test.ts @@ -0,0 +1,503 @@ +import EmberObject from '@ember/object'; +import { settled } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases'; +import type { Change } from '@warp-drive/core-types/cache/change'; +import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; +import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; +import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; +import type { + RecordIdentifier, + StableDocumentIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@warp-drive/core-types/identifier'; +import type { + CollectionResourceDataDocument, + ResourceDocument, + ResourceErrorDocument, + ResourceMetaDocument, + SingleResourceDataDocument, +} from '@warp-drive/core-types/spec/document'; +import type { ApiError } from '@warp-drive/core-types/spec/error'; +import type { + CollectionResourceDocument, + ExistingResourceObject, + JsonApiDocument, + SingleResourceDocument, +} from '@warp-drive/core-types/spec/json-api-raw'; +import { Type } from '@warp-drive/core-types/symbols'; + +class Person extends Model { + // TODO fix the typing for naked attrs + @attr('string', {}) + name; + + declare [Type]: 'person'; +} + +class House extends Model { + // TODO fix the typing for naked attrs + @attr('string', {}) + name; + + @belongsTo('person', { async: false, inverse: null }) + landlord; + + @hasMany('person', { async: false, inverse: null }) + tenants; +} + +class TestCache implements Cache { + version = '2' as const; + + _errors?: ApiError[]; + _isNew = false; + _storeWrapper: CacheCapabilitiesManager; + _identifier: StableRecordIdentifier; + + constructor(wrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier) { + this._storeWrapper = wrapper; + this._identifier = identifier; + } + changedRelationships(identifier: StableRecordIdentifier): Map { + throw new Error('Method not implemented.'); + } + hasChangedRelationships(identifier: StableRecordIdentifier): boolean { + throw new Error('Method not implemented.'); + } + rollbackRelationships(identifier: StableRecordIdentifier): string[] { + throw new Error('Method not implemented.'); + } + patch(op: MergeOperation): void { + throw new Error('Method not implemented.'); + } + _data: Map = new Map(); + put(doc: StructuredDocument): SingleResourceDataDocument; + put(doc: StructuredDocument): CollectionResourceDataDocument; + put( + doc: StructuredDocument + ): ResourceMetaDocument | ResourceErrorDocument; + put(doc: StructuredDocument): ResourceDocument { + if ('content' in doc && !('error' in doc)) { + if (Array.isArray(doc.content.data)) { + const data = doc.content.data.map((resource) => { + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + resource + ) as StableExistingRecordIdentifier; + this.upsert(identifier, resource, this._storeWrapper.hasRecord(identifier)); + return identifier; + }); + return { data }; + } else { + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + doc.content.data as RecordIdentifier + ); + this.upsert(identifier, doc.content.data!, this._storeWrapper.hasRecord(identifier)); + return { data: identifier } as SingleResourceDataDocument; + } + } else if ('error' in doc) { + throw typeof doc.error === 'string' ? new Error(doc.error) : (doc.error as Error); + } + throw new Error('Not Implemented'); + } + + peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { + throw new Error(`Not Implemented`); + } + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { + throw new Error(`Not Implemented`); + } + fork(): Promise { + throw new Error(`Not Implemented`); + } + merge(cache: Cache): Promise { + throw new Error(`Not Implemented`); + } + diff(): Promise { + throw new Error(`Not Implemented`); + } + dump(): Promise> { + throw new Error(`Not Implemented`); + } + hydrate(stream: ReadableStream): Promise { + throw new Error('Not Implemented'); + } + + upsert( + identifier: StableRecordIdentifier, + data: ExistingResourceObject, + calculateChanges?: boolean + ): void | string[] { + if (!this._data.has(identifier)) { + this._storeWrapper.notifyChange(identifier, 'added'); + } + this._data.set(identifier, data); + this._storeWrapper.notifyChange(identifier, 'attributes'); + this._storeWrapper.notifyChange(identifier, 'relationships'); + } + + clientDidCreate(identifier: StableRecordIdentifier, options?: Record): Record { + this._isNew = true; + return {}; + } + willCommit(identifier: StableRecordIdentifier): void {} + didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { + return { data: identifier as StableExistingRecordIdentifier }; + } + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void { + this._errors = errors; + } + unloadRecord(identifier: StableRecordIdentifier): void {} + getAttr(identifier: StableRecordIdentifier, propertyName: string): string { + return ''; + } + setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { + throw new Error('Method not implemented.'); + } + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { + return {}; + } + hasChangedAttrs(identifier: StableRecordIdentifier): boolean { + return false; + } + rollbackAttrs(identifier: StableRecordIdentifier): string[] { + return []; + } + getRelationship( + identifier: StableRecordIdentifier, + propertyName: string + ): ResourceRelationship | CollectionRelationship { + throw new Error('Method not implemented.'); + } + mutate(operation: LocalRelationshipOperation): void { + throw new Error('Method not implemented.'); + } + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { + throw new Error('Method not implemented.'); + } + + getErrors(identifier: StableRecordIdentifier): ApiError[] { + return this._errors || []; + } + isEmpty(identifier: StableRecordIdentifier): boolean { + return false; + } + isNew(identifier: StableRecordIdentifier): boolean { + return this._isNew; + } + isDeleted(identifier: StableRecordIdentifier): boolean { + return false; + } + isDeletionCommitted(identifier: StableRecordIdentifier): boolean { + return false; + } +} + +module('integration/record-data - Custom Cache Implementations', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + + owner.register('model:person', Person); + owner.register('model:house', House); + // @ts-expect-error missing type + + owner.unregister('service:store'); + owner.register('service:store', Store); + owner.register('adapter:application', class extends JSONAPIAdapter {}); + owner.register('serializer:application', class extends JSONAPISerializer {}); + }); + + test('A Cache implementation that has the required spec methods should not error out', async function (assert) { + const { owner } = this; + const store: Store = owner.lookup('service:store') as unknown as Store; + + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Scumbag Katz', + }, + }, + ], + }); + + const all = store.peekAll('person'); + assert.strictEqual(all.length, 2, 'we have 2 records'); + + store.push({ + data: [ + { + type: 'person', + id: '3', + attributes: { + name: 'Scumbag Bryn', + }, + }, + ], + }); + + await settled(); + + assert.strictEqual(all.length, 3, 'we have 3 records'); + }); + + test('Record Data push, create and save lifecycle', async function (assert) { + assert.expect(19); + let called = 0; + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }; + const { owner } = this; + let calledUpsert = 0; + let calledClientDidCreate = 0; + let calledWillCommit = 0; + let calledWasRejected = 0; + let calledUnloadRecord = 0; + let calledRollbackAttributes = 0; + let calledDidCommit = 0; + let isNew = false; + + class LifecycleCache extends TestCache { + override upsert() { + calledUpsert++; + } + + override clientDidCreate( + identifier: StableRecordIdentifier, + options?: Record + ): Record { + calledClientDidCreate++; + isNew = true; + return {}; + } + + override willCommit() { + calledWillCommit++; + } + + override commitWasRejected(identifier: StableRecordIdentifier, errors: ApiError[] | undefined) { + super.commitWasRejected(identifier, errors); + calledWasRejected++; + } + + override unloadRecord() { + calledUnloadRecord++; + } + + override rollbackAttrs() { + calledRollbackAttributes++; + return []; + } + rollbackAttributes() { + calledRollbackAttributes++; + } + + override didCommit(identifier: StableExistingRecordIdentifier, result: StructuredDataDocument) { + calledDidCommit++; + isNew = false; + return { data: identifier }; + } + + override isNew() { + return isNew; + } + } + + class TestStore extends Store { + override createCache(storeWrapper: CacheCapabilitiesManager) { + // @ts-expect-error + return new LifecycleCache(storeWrapper) as Cache; + } + } + + const TestAdapter = EmberObject.extend({ + updateRecord() { + called++; + if (called === 1) { + return Promise.resolve(); + } else if (called > 1) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject(); + } + }, + + createRecord() { + return Promise.resolve(); + }, + }); + + owner.register('service:store', TestStore); + owner.register('adapter:application', TestAdapter, { singleton: false }); + + const store = owner.lookup('service:store') as unknown as Store; + + store.push({ + data: [personHash], + }); + assert.strictEqual(calledUpsert, 1, 'Called upsert'); + + const person = store.peekRecord('person', '1') as Model; + void person.save(); + assert.strictEqual(calledWillCommit, 1, 'Called willCommit'); + + await settled(); + assert.strictEqual(calledDidCommit, 1, 'Called didCommit'); + + let promise = person.save(); + assert.strictEqual(calledWillCommit, 2, 'Called willCommit'); + + await promise.catch((_e) => assert.ok(true, 'we erred')); + + assert.strictEqual(calledDidCommit, 1, 'Did not call didCommit again'); + assert.strictEqual(calledWasRejected, 1, 'Called commitWasRejected'); + + person.rollbackAttributes(); + assert.strictEqual(calledRollbackAttributes, 1, 'Called rollbackAttributes'); + + person.unloadRecord(); + assert.strictEqual(calledUnloadRecord, 1, 'Called unloadRecord'); + + await settled(); + assert.strictEqual(calledClientDidCreate, 0, 'Did not called clientDidCreate'); + + calledUpsert = 0; + calledClientDidCreate = 0; + calledWillCommit = 0; + calledWasRejected = 0; + calledUnloadRecord = 0; + calledRollbackAttributes = 0; + calledDidCommit = 0; + + const clientPerson = store.createRecord('person', { id: '2' }) as Model; + assert.strictEqual(calledClientDidCreate, 1, 'Called clientDidCreate'); + + void clientPerson.save(); + assert.strictEqual(calledWillCommit, 1, 'Called willCommit'); + + await settled(); + assert.strictEqual(calledDidCommit, 1, 'Called didCommit'); + + promise = clientPerson.save(); + assert.strictEqual(calledWillCommit, 2, 'Called willCommit'); + + await promise.catch((_e) => assert.ok('we erred')); + assert.strictEqual(calledWasRejected, 1, 'Called commitWasRejected'); + assert.strictEqual(calledDidCommit, 1, 'Did not call didCommit again'); + + clientPerson.unloadRecord(); + assert.strictEqual(calledUnloadRecord, 1, 'Called unloadRecord'); + + await settled(); + assert.strictEqual(calledUpsert, 0, 'Did not call pushData'); + }); + + test('Record Data attribute setting', function (assert) { + const expectedCount = 13; + assert.expect(expectedCount); + const personHash = { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }; + + const { owner } = this; + let calledGet = 0; + + class AttributeCache extends TestCache { + changedAttributes() { + return { name: ['old', 'new'] as [string, string] }; + } + + hasChangedAttributes(): boolean { + return false; + } + + override changedAttrs() { + return { name: ['old', 'new'] as [string, string] }; + } + + override hasChangedAttrs(): boolean { + return false; + } + + override setAttr(identifier: StableRecordIdentifier, key: string, value: unknown) { + assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); + assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); + } + + setDirtyAttribute(key: string, value: unknown) { + assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); + assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); + } + + override getAttr(identifier: StableRecordIdentifier, key: string): string { + calledGet++; + assert.strictEqual(key, 'name', 'key passed to getAttr'); + + return 'new attribute'; + } + } + + class TestStore extends Store { + override createCache(storeWrapper: CacheCapabilitiesManager) { + // @ts-expect-error + return new AttributeCache(storeWrapper) as Cache; + } + } + + owner.register('service:store', TestStore); + + const store = owner.lookup('service:store') as unknown as Store; + + store.push({ + data: [personHash], + }); + + const person = store.peekRecord('person', '1')!; + assert.strictEqual(person.name, 'new attribute'); + assert.strictEqual(calledGet, 1, 'called getAttr for initial get'); + person.set('name', 'new value'); + assert.strictEqual(calledGet, 2, 'called getAttr during set'); + assert.strictEqual(person.name, 'new value'); + assert.strictEqual(calledGet, 2, 'did not call getAttr after set'); + person.notifyPropertyChange('name'); + assert.strictEqual(person.name, 'new attribute'); + assert.strictEqual(calledGet, 3, 'called getAttr after notifyPropertyChange'); + assert.deepEqual( + person.changedAttributes(), + { name: ['old', 'new'] }, + 'changed attributes passes through RD value' + ); + }); +}); diff --git a/tests/main/tests/integration/client-id-generation-test.js b/tests/main/tests/integration/client-id-generation-test.js index 5f4e857442e..a7833aaf62b 100644 --- a/tests/main/tests/integration/client-id-generation-test.js +++ b/tests/main/tests/integration/client-id-generation-test.js @@ -1,13 +1,13 @@ import { get } from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; 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('integration - Client Id Generation', function (hooks) { setupTest(hooks); @@ -15,7 +15,7 @@ module('integration - Client Id Generation', function (hooks) { let adapter; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; class Comment extends Model { @attr() @@ -56,11 +56,11 @@ module('integration - Client Id Generation', function (hooks) { }; adapter.createRecord = function (store, modelClass, snapshot) { - let type = modelClass.modelName; + const type = modelClass.modelName; if (type === 'comment') { assert.strictEqual(snapshot.id, 'id-1', "Comment passed to `createRecord` has 'id-1' assigned"); - return resolve({ + return Promise.resolve({ data: { type, id: snapshot.id, @@ -68,7 +68,7 @@ module('integration - Client Id Generation', function (hooks) { }); } else { assert.strictEqual(snapshot.id, 'id-2', "Post passed to `createRecord` has 'id-2' assigned"); - return resolve({ + return Promise.resolve({ data: { type, id: snapshot.id, @@ -77,8 +77,8 @@ module('integration - Client Id Generation', function (hooks) { } }; - let comment = store.createRecord('comment'); - let post = store.createRecord('post'); + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); assert.strictEqual(get(comment, 'id'), 'id-1', "comment is assigned id 'id-1'"); assert.strictEqual(get(post, 'id'), 'id-2', "post is assigned id 'id-2'"); @@ -89,32 +89,40 @@ module('integration - Client Id Generation', function (hooks) { await post.save(); }); - test('empty string and undefined ids should coerce to null', async function (assert) { - assert.expect(6); - let idCount = 0; - let id = 1; - let ids = [undefined, '']; - - adapter.generateIdForRecord = function (passedStore, record) { - assert.strictEqual(store, passedStore, 'store is the first parameter'); - - return ids[idCount++]; - }; - - adapter.createRecord = function (store, type, record) { - assert.strictEqual(typeof get(record, 'id'), 'object', 'correct type'); - return resolve({ data: { id: id++, type: type.modelName } }); - }; - - let comment = store.createRecord('misc'); - let post = store.createRecord('misc'); - - assert.strictEqual(get(comment, 'id'), null, "comment is assigned id 'null'"); - assert.strictEqual(get(post, 'id'), null, "post is assigned id 'null'"); - - // Despite client-generated IDs, calling commit() on the store should still - // invoke the adapter's `createRecord` method. - await comment.save(); - await post.save(); - }); + deprecatedTest( + 'empty string and undefined ids should coerce to null', + { + count: 2, + until: '6.0', + id: 'ember-data:deprecate-non-strict-id', + }, + async function (assert) { + assert.expect(6); + let idCount = 0; + let id = 1; + const ids = [undefined, '']; + + adapter.generateIdForRecord = function (passedStore, record) { + assert.strictEqual(store, passedStore, 'store is the first parameter'); + + return ids[idCount++]; + }; + + adapter.createRecord = function (store, type, record) { + assert.strictEqual(typeof get(record, 'id'), 'object', 'correct type'); + return Promise.resolve({ data: { id: id++, type: type.modelName } }); + }; + + const comment = store.createRecord('misc'); + const post = store.createRecord('misc'); + + assert.strictEqual(get(comment, 'id'), null, "comment is assigned id 'null'"); + assert.strictEqual(get(post, 'id'), null, "post is assigned id 'null'"); + + // Despite client-generated IDs, calling commit() on the store should still + // invoke the adapter's `createRecord` method. + await comment.save(); + await post.save(); + } + ); }); diff --git a/tests/main/tests/integration/debug-adapter-test.js b/tests/main/tests/integration/debug-adapter-test.js index 3debdb2ffc8..1eb1a1bf88f 100644 --- a/tests/main/tests/integration/debug-adapter-test.js +++ b/tests/main/tests/integration/debug-adapter-test.js @@ -3,207 +3,227 @@ import { get } from '@ember/object'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import require, { has } from 'require'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; +// TODO move these tests to the DEBUG package +import DebugAdapter from '@ember-data/debug'; import Model, { attr } from '@ember-data/model'; -// TODO move these tests to the DEBUG package -if (has('@ember-data/debug')) { - const DebugAdapter = require('@ember-data/debug').default; +module('integration/debug-adapter - DebugAdapter', function (hooks) { + setupTest(hooks); - module('integration/debug-adapter - DebugAdapter', function (hooks) { - setupTest(hooks); + let store; - let store; + hooks.beforeEach(function () { + const { owner } = this; + class Post extends Model { + @attr title; + } - hooks.beforeEach(function () { - let { owner } = this; - class Post extends Model { - @attr title; - } + owner.register('model:post', Post); + store = owner.lookup('service:store'); + const _adapter = DebugAdapter.extend({ + getModelTypes() { + return A([{ klass: store.modelFor('post'), name: 'post' }]); + }, + }); + owner.register('data-adapter:main', _adapter); + }); - owner.register('model:post', Post); - store = owner.lookup('service:store'); - let _adapter = DebugAdapter.extend({ - getModelTypes() { - return A([{ klass: store.modelFor('post'), name: 'post' }]); + test('Watching Types Seen Before Inspector Initialized', async function (assert) { + assert.expect(4); + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Post Title', }, - }); - owner.register('data-adapter:main', _adapter); + }, }); + await settled(); + + const debugAdapter = this.owner.lookup('data-adapter:main'); + function added(types) { + assert.strictEqual(types.length, 1, 'added one type'); + assert.strictEqual(types[0].name, 'post', 'the type is post'); + assert.strictEqual(types[0].count, 1, 'we added one post'); + assert.strictEqual(types[0].object, store.modelFor('post'), 'we received the ModelClass for post'); + } + + debugAdapter.watchModelTypes(added, () => null); + }); - test('Watching Model Types', async function (assert) { - assert.expect(4); - let { owner } = this; - let debugAdapter = owner.lookup('data-adapter:main'); - - function added(types) { - assert.strictEqual(types.length, 1, 'added one type'); - assert.strictEqual(types[0].name, 'post', 'the type is post'); - assert.strictEqual(types[0].count, 1, 'we added one post'); - assert.strictEqual(types[0].object, store.modelFor('post'), 'we received the ModelClass for post'); - } - - debugAdapter.watchModelTypes(added, () => null); - - store.push({ - data: { - type: 'post', - id: '1', - attributes: { - title: 'Post Title', - }, + test('Watching Model Types', async function (assert) { + assert.expect(4); + const { owner } = this; + const debugAdapter = owner.lookup('data-adapter:main'); + + function added(types) { + assert.strictEqual(types.length, 1, 'added one type'); + assert.strictEqual(types[0].name, 'post', 'the type is post'); + assert.strictEqual(types[0].count, 1, 'we added one post'); + assert.strictEqual(types[0].object, store.modelFor('post'), 'we received the ModelClass for post'); + } + + debugAdapter.watchModelTypes(added, () => null); + + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Post Title', }, - }); + }, }); + }); - test('Watching Model Types On first-create', async function (assert) { - assert.expect(4); - let { owner } = this; - let debugAdapter = owner.lookup('data-adapter:main'); + test('Watching Model Types On first-create', async function (assert) { + assert.expect(4); + const { owner } = this; + const debugAdapter = owner.lookup('data-adapter:main'); - function added(types) { - assert.strictEqual(types.length, 1, 'added one type'); - assert.strictEqual(types[0].name, 'post', 'the type is post'); - assert.strictEqual(types[0].count, 1, 'we added one posts'); - assert.strictEqual(types[0].object, store.modelFor('post'), 'we received the ModelClass for post'); - } + function added(types) { + assert.strictEqual(types.length, 1, 'added one type'); + assert.strictEqual(types[0].name, 'post', 'the type is post'); + assert.strictEqual(types[0].count, 1, 'we added one posts'); + assert.strictEqual(types[0].object, store.modelFor('post'), 'we received the ModelClass for post'); + } - debugAdapter.watchModelTypes(added, () => null); + debugAdapter.watchModelTypes(added, () => null); - store.createRecord('post', { - title: 'Post Title', - }); + store.createRecord('post', { + title: 'Post Title', }); + }); - test('Watching Records', async function (assert) { - let { owner } = this; - let debugAdapter = owner.lookup('data-adapter:main'); - let addedRecords, updatedRecords, removedRecords; - - this.owner.register( - 'adapter:application', - Adapter.extend({ - shouldBackgroundReloadRecord() { - return false; - }, - }) - ); - - store.push({ - data: { - type: 'post', - id: '1', - attributes: { - title: 'Clean Post', - }, + test('Watching Records', async function (assert) { + const { owner } = this; + const debugAdapter = owner.lookup('data-adapter:main'); + let addedRecords, updatedRecords, removedRecords; + + this.owner.register( + 'adapter:application', + Adapter.extend({ + shouldBackgroundReloadRecord() { + return false; + }, + }) + ); + + store.push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Clean Post', }, - }); - - let recordsAdded = function (wrappedRecords) { - addedRecords = wrappedRecords; - }; - let recordsUpdated = function (wrappedRecords) { - updatedRecords = wrappedRecords; - }; - let recordsRemoved = function (...args) { - // in 3.26 there is only 1 argument - the record removed - // below 3.26, it is 2 arguments - the index and count removed - // https://github.com/emberjs/ember.js/pull/19379 - removedRecords = args; - }; - - debugAdapter.watchRecords('post', recordsAdded, recordsUpdated, recordsRemoved); - - assert.strictEqual(get(addedRecords, 'length'), 1, 'We initially have 1 post'); - let record = addedRecords[0]; - assert.deepEqual(record.columnValues, { id: '1', title: 'Clean Post' }, 'The initial post has the right values'); - assert.deepEqual( - record.filterValues, - { isNew: false, isModified: false, isClean: true }, - 'The initial post has the right state' - ); - assert.deepEqual(record.searchKeywords, ['1', 'Clean Post'], 'We have meaningful keywords'); - assert.deepEqual(record.color, 'black', 'We are given the right display color for a clean value'); - - let post = await store.findRecord('post', 1); - - post.set('title', 'Modified Post'); - - // await updated callback - await settled(); - - assert.strictEqual(get(updatedRecords, 'length'), 1, 'We updated 1 post'); - record = updatedRecords[0]; - assert.deepEqual( - record.columnValues, - { id: '1', title: 'Modified Post' }, - 'The modified values are correct for the post' - ); - assert.deepEqual( - record.filterValues, - { isNew: false, isModified: true, isClean: false }, - 'The modified state is correct for the post' - ); - assert.deepEqual(record.searchKeywords, ['1', 'Modified Post'], 'The keywords have been updated'); - assert.deepEqual(record.color, 'blue', 'we have a color to represent we were modified'); - - // reset - addedRecords = updatedRecords = []; - - post = store.createRecord('post', { id: '2', title: 'New Post' }); - - await settled(); - - assert.strictEqual(get(addedRecords, 'length'), 1, 'We are notified when we add a newly created post'); - record = addedRecords[0]; - assert.deepEqual( - record && record.columnValues, - { id: '2', title: 'New Post' }, - 'The newly created post has the right values' - ); - assert.deepEqual( - record && record.filterValues, - { isNew: true, isModified: false, isClean: false }, - 'The newly created post has the right state' - ); - assert.deepEqual( - record && record.searchKeywords, - ['2', 'New Post'], - 'The newly created post has meaningful keywords' - ); - assert.deepEqual(record && record.color, 'green'), - 'The newly created post has meaningful color to represent new-ness'; - - // reset - addedRecords = updatedRecords = []; - - post.unloadRecord(); - - await settled(); - - assert.strictEqual(removedRecords.length, 1, 'We are notified of the total posts removed'); - assert.strictEqual(removedRecords[0][0].object, post, 'The removed post is correct'); + }, }); - test('Column names', function (assert) { - let { owner } = this; - let debugAdapter = owner.lookup('data-adapter:main'); - class Person extends Model { - @attr title; - @attr firstOrLastName; - } - owner.register('model:person', Person); - const store = owner.lookup('service:store'); - - const columns = debugAdapter.columnsForType(store.modelFor('person')); - - assert.strictEqual(columns[0].desc, 'Id'); - assert.strictEqual(columns[1].desc, 'Title'); - assert.strictEqual(columns[2].desc, 'First or last name'); - }); + const recordsAdded = function (wrappedRecords) { + addedRecords = wrappedRecords; + }; + const recordsUpdated = function (wrappedRecords) { + updatedRecords = wrappedRecords; + }; + const recordsRemoved = function (...args) { + // in 3.26 there is only 1 argument - the record removed + // below 3.26, it is 2 arguments - the index and count removed + // https://github.com/emberjs/ember.js/pull/19379 + removedRecords = args; + }; + + debugAdapter.watchRecords('post', recordsAdded, recordsUpdated, recordsRemoved); + + assert.strictEqual(get(addedRecords, 'length'), 1, 'We initially have 1 post'); + let record = addedRecords[0]; + assert.deepEqual(record.columnValues, { id: '1', title: 'Clean Post' }, 'The initial post has the right values'); + assert.deepEqual( + record.filterValues, + { isNew: false, isModified: false, isClean: true }, + 'The initial post has the right state' + ); + assert.deepEqual(record.searchKeywords, ['1', 'Clean Post'], 'We have meaningful keywords'); + assert.deepEqual(record.color, 'black', 'We are given the right display color for a clean value'); + + let post = await store.findRecord('post', 1); + + post.set('title', 'Modified Post'); + + // await updated callback + await settled(); + + assert.strictEqual(get(updatedRecords, 'length'), 1, 'We updated 1 post'); + record = updatedRecords[0]; + assert.deepEqual( + record.columnValues, + { id: '1', title: 'Modified Post' }, + 'The modified values are correct for the post' + ); + assert.deepEqual( + record.filterValues, + { isNew: false, isModified: true, isClean: false }, + 'The modified state is correct for the post' + ); + assert.deepEqual(record.searchKeywords, ['1', 'Modified Post'], 'The keywords have been updated'); + assert.deepEqual(record.color, 'blue', 'we have a color to represent we were modified'); + + // reset + addedRecords = updatedRecords = []; + + post = store.createRecord('post', { id: '2', title: 'New Post' }); + + await settled(); + + assert.strictEqual(get(addedRecords, 'length'), 1, 'We are notified when we add a newly created post'); + record = addedRecords[0]; + assert.deepEqual( + record && record.columnValues, + { id: '2', title: 'New Post' }, + 'The newly created post has the right values' + ); + assert.deepEqual( + record && record.filterValues, + { isNew: true, isModified: false, isClean: false }, + 'The newly created post has the right state' + ); + assert.deepEqual( + record && record.searchKeywords, + ['2', 'New Post'], + 'The newly created post has meaningful keywords' + ); + assert.deepEqual(record && record.color, 'green'), + 'The newly created post has meaningful color to represent new-ness'; + + // reset + addedRecords = updatedRecords = []; + + post.unloadRecord(); + + await settled(); + + assert.strictEqual(removedRecords.length, 1, 'We are notified of the total posts removed'); + assert.strictEqual(removedRecords[0][0].object, post, 'The removed post is correct'); + }); + + test('Column names', function (assert) { + const { owner } = this; + const debugAdapter = owner.lookup('data-adapter:main'); + class Person extends Model { + @attr title; + @attr firstOrLastName; + } + owner.register('model:person', Person); + const store = owner.lookup('service:store'); + + const columns = debugAdapter.columnsForType(store.modelFor('person')); + + assert.strictEqual(columns[0].desc, 'Id'); + assert.strictEqual(columns[1].desc, 'Title'); + assert.strictEqual(columns[2].desc, 'First or last name'); }); -} +}); diff --git a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts index 41112c4f72e..398d7dec740 100644 --- a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts +++ b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts @@ -2,11 +2,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { DEBUG } from '@ember-data/env'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr, belongsTo } from '@ember-data/model'; import type Store from '@ember-data/store'; -import type { ModelSchema } from '@ember-data/types/q/ds-model'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; let IS_DEBUG = false; @@ -43,7 +43,7 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); @@ -67,12 +67,13 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load and later updated remotely', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); // access the relationship before load try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions user.bestFriend; // in IS_DEBUG we error and should not reach here @@ -109,12 +110,13 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load and later mutated', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); // access the relationship before load try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions user.bestFriend; // in IS_DEBUG we error and should not reach here @@ -124,7 +126,7 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); } - const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + const peter = store.createRecord('user', { name: 'Peter' }) as User; user.bestFriend = peter; // access the relationship again @@ -135,7 +137,7 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; // access the relationship before load try { @@ -173,11 +175,11 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load and then later attempted to be found via findRecord', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; this.owner.register( 'adapter:application', class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { assert.step('findRecord'); assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); return Promise.resolve({ @@ -231,11 +233,11 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { test('When a sync relationship is accessed before load and a later attempt to load via findRecord errors', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; this.owner.register( 'adapter:application', class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { assert.step('findRecord'); assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); @@ -272,6 +274,7 @@ module('Emergent Behavior > Recovery | belongsTo', function (hooks) { // access the relationship after sideload try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions user.bestFriend; // in production we do not error diff --git a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts index e34df58505b..87c26dffd78 100644 --- a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts +++ b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts @@ -2,11 +2,12 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { DEBUG } from '@ember-data/env'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; -import Model, { attr, hasMany } from '@ember-data/model'; +import Model, { attr, type HasMany, hasMany } from '@ember-data/model'; import type Store from '@ember-data/store'; -import type { ModelSchema } from '@ember-data/types/q/ds-model'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { Type } from '@warp-drive/core-types/symbols'; let IS_DEBUG = false; @@ -14,9 +15,10 @@ if (DEBUG) { IS_DEBUG = true; } class User extends Model { + declare [Type]: 'user'; @attr declare name: string; - @hasMany('user', { async: false, inverse: null }) declare friends: User[]; - @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: User[]; + @hasMany('user', { async: false, inverse: null }) declare friends: HasMany; + @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: HasMany; } module('Emergent Behavior > Recovery | hasMany', function (hooks) { @@ -48,7 +50,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); @@ -81,7 +83,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and later updated remotely', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); @@ -151,7 +153,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load, records are later loaded, and then it is updated by related record deletion', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); @@ -228,7 +230,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { this.owner.register( 'adapter:application', class { - deleteRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + deleteRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { return Promise.resolve({ data: null, }); @@ -268,7 +270,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and later updated by remote inverse removal', function (assert) { class LocalUser extends Model { @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; } this.owner.register('model:local-user', LocalUser); const store = this.owner.lookup('service:store') as Store; @@ -303,8 +305,8 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { }, }, ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + }) as LocalUser; + const user2 = store.peekRecord('local-user', '4') as LocalUser; assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); @@ -368,7 +370,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and later mutated directly', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); @@ -395,7 +397,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { } assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); - const peter = store.createRecord('user', { name: 'Peter' }) as unknown as User; + const peter = store.createRecord('user', { name: 'Peter' }); try { user.friends.push(peter); @@ -432,12 +434,13 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and later mutated via add by inverse', function (assert) { class LocalUser extends Model { + declare [Type]: 'local-user'; @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; } this.owner.register('model:local-user', LocalUser); const store = this.owner.lookup('service:store') as Store; - const user1 = store.push({ + const user1 = store.push({ data: { type: 'local-user', id: '1', @@ -468,11 +471,11 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { }, }, ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '5') as unknown as LocalUser; + }); + const user2 = store.peekRecord('local-user', '5'); assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); - assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + assert.strictEqual(user2!.name, 'Krystan', 'precond2 - user is loaded'); // access the relationship before load try { @@ -500,7 +503,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { // add user2 to user1's friends via inverse try { - user2.friends.push(user1); + user2!.friends.push(user1); assert.ok(true, 'mutating the relationship should not throw'); } catch (e) { assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); @@ -534,12 +537,13 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and later mutated via remove by inverse', function (assert) { class LocalUser extends Model { + declare [Type]: 'local-user'; @attr declare name: string; - @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: LocalUser[]; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; } this.owner.register('model:local-user', LocalUser); const store = this.owner.lookup('service:store') as Store; - const user1 = store.push({ + const user1 = store.push({ data: { type: 'local-user', id: '1', @@ -570,8 +574,8 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { }, }, ], - }) as unknown as LocalUser; - const user2 = store.peekRecord('local-user', '4') as unknown as LocalUser; + }); + const user2 = store.peekRecord('local-user', '4')!; assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); @@ -637,7 +641,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; // access the relationship before load try { @@ -752,11 +756,11 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: null)', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; this.owner.register( 'adapter:application', class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { assert.step('findRecord'); assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); return Promise.resolve({ @@ -922,7 +926,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: specified)', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; store.push({ data: { type: 'user', @@ -944,7 +948,7 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { this.owner.register( 'adapter:application', class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { assert.step('findRecord'); if (snapshot.include === 'frenemies') { assert.deepEqual(snapshot._attributes, { name: 'Rey' }, 'the snapshot has the correct attributes'); @@ -1166,11 +1170,11 @@ module('Emergent Behavior > Recovery | hasMany', function (hooks) { test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const user = store.peekRecord('user', '1') as unknown as User; + const user = store.peekRecord('user', '1') as User; this.owner.register( 'adapter:application', class { - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { assert.step('findRecord'); assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); diff --git a/tests/main/tests/integration/identifiers/cache-test.ts b/tests/main/tests/integration/identifiers/cache-test.ts index 1e13e188633..ec626c3ae7f 100644 --- a/tests/main/tests/integration/identifiers/cache-test.ts +++ b/tests/main/tests/integration/identifiers/cache-test.ts @@ -2,19 +2,14 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import type Store from '@ember-data/store'; +import { test as testInDebug } from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('Integration | Identifiers - cache', function (hooks) { setupTest(hooks); - let store, cache; - - hooks.beforeEach(function () { - store = this.owner.lookup('service:store'); - cache = store.identifierCache; - }); module('getOrCreateRecordIdentifier()', function () { - test('creates a new resource identifier if forgetRecordIdentifier() has been called on the existing identifier', async function (assert) { + test('creates a new resource identifier if forgetRecordIdentifier() has been called on the existing identifier', function (assert) { const runspiredHash = { type: 'person', id: '1', @@ -22,6 +17,8 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; const identifier = cache.getOrCreateRecordIdentifier(runspiredHash); cache.forgetRecordIdentifier(identifier); @@ -35,7 +32,7 @@ module('Integration | Identifiers - cache', function (hooks) { ); }); - test('returns the existing identifier when called with an identifier', async function (assert) { + test('returns the existing identifier when called with an identifier', function (assert) { const houseHash = { type: 'house', id: '1', @@ -43,6 +40,7 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'Moomin', }, }; + const store = this.owner.lookup('service:store') as Store; const cache = store.identifierCache; const identifier = cache.getOrCreateRecordIdentifier(houseHash); @@ -53,7 +51,7 @@ module('Integration | Identifiers - cache', function (hooks) { ); }); - test('identifiers are cached by lid and can be looked up by lid', async function (assert) { + test('identifiers are cached by lid and can be looked up by lid', function (assert) { const houseHash = { type: 'house', id: '1', @@ -61,6 +59,7 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'Moomin', }, }; + const store = this.owner.lookup('service:store') as Store; const cache = store.identifierCache; const identifier = cache.getOrCreateRecordIdentifier(houseHash); @@ -79,7 +78,7 @@ module('Integration | Identifiers - cache', function (hooks) { }); module('createIdentifierForNewRecord()', function () { - test('returns new identifier', async function (assert) { + test('returns new identifier', function (assert) { const runspiredHash = { type: 'person', id: '1', @@ -87,6 +86,8 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; const identifier = cache.createIdentifierForNewRecord(runspiredHash); assert.strictEqual(identifier.id, '1', 'identifier has id'); @@ -96,7 +97,7 @@ module('Integration | Identifiers - cache', function (hooks) { }); module('updateRecordIdentifier()', function () { - test('returns same identifier', async function (assert) { + test('returns same identifier', function (assert) { const runspiredHash = { type: 'person', id: '1', @@ -104,15 +105,17 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; - let identifier = cache.createIdentifierForNewRecord(runspiredHash); + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; + const identifier = cache.createIdentifierForNewRecord(runspiredHash); - let mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: '1' }); + const mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: '1' }); assert.strictEqual(mergedIdentifier.id, identifier.id, 'merged identifier has same id'); assert.strictEqual(mergedIdentifier.type, identifier.type, 'merged identifier has same type'); }); - test('returns new identifier with different id', async function (assert) { + test('returns new identifier with different id', function (assert) { const runspiredHash = { type: 'person', id: '1', @@ -120,9 +123,11 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; - let identifier = cache.createIdentifierForNewRecord(runspiredHash); + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; + const identifier = cache.createIdentifierForNewRecord(runspiredHash); - let mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: '2' }); + const mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: '2' }); assert.strictEqual(mergedIdentifier.id, '2', 'merged identifier has new id'); assert.strictEqual(mergedIdentifier.type, 'person', 'merged identifier has same type'); @@ -136,13 +141,15 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; cache.createIdentifierForNewRecord(runspiredHash); - assert.expectAssertion(() => { + await assert.expectAssertion(() => { cache.createIdentifierForNewRecord(runspiredHash); }, 'The lid generated for the new record is not unique as it matches an existing identifier'); }); - test('id is null', async function (assert) { + test('id is null', function (assert) { const runspiredHash = { type: 'person', id: '1', @@ -150,9 +157,11 @@ module('Integration | Identifiers - cache', function (hooks) { name: 'runspired', }, }; - let identifier = cache.createIdentifierForNewRecord(runspiredHash); + const store = this.owner.lookup('service:store') as Store; + const cache = store.identifierCache; + const identifier = cache.createIdentifierForNewRecord(runspiredHash); - let mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: null }); + const mergedIdentifier = cache.updateRecordIdentifier(identifier, { type: 'person', id: null }); assert.strictEqual(mergedIdentifier.id, null, 'merged identifier has null id'); assert.strictEqual(mergedIdentifier.type, identifier.type, 'merged identifier has same type'); diff --git a/tests/main/tests/integration/identifiers/configuration-test.ts b/tests/main/tests/integration/identifiers/configuration-test.ts index 606bea2fb1c..4c9924b5505 100644 --- a/tests/main/tests/integration/identifiers/configuration-test.ts +++ b/tests/main/tests/integration/identifiers/configuration-test.ts @@ -1,31 +1,27 @@ import EmberObject, { set } from '@ember/object'; -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { all, resolve } from 'rsvp'; +import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; -import Store, { +import { recordIdentifierFor, setIdentifierForgetMethod, setIdentifierGenerationMethod, setIdentifierResetMethod, setIdentifierUpdateMethod, } from '@ember-data/store'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import type { - GenerationMethod, - IdentifierBucket, - ResourceData, - StableIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; +import type { IdentifierBucket, StableIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ExistingResourceObject, ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; + +type ResourceData = ResourceIdentifierObject | ExistingResourceObject; +type GenerationMethod = Parameters[0]; module('Integration | Identifiers - configuration', function (hooks) { setupTest(hooks); @@ -33,7 +29,7 @@ module('Integration | Identifiers - configuration', function (hooks) { hooks.beforeEach(function () { const { owner } = this; - owner.register('adapter:application', JSONAPIAdapter.extend()); + owner.register('adapter:application', class extends JSONAPIAdapter {}); owner.register('serializer:application', class extends JSONAPISerializer {}); class User extends Model { @attr() @@ -79,8 +75,8 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierForgetMethod(null); }); - test(`The configured generation method is used for pushed records`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + test(`The configured generation method is used for pushed records`, function (assert) { + const store = this.owner.lookup('service:store') as unknown as Store; const record = store.push({ data: { type: 'user', @@ -96,7 +92,7 @@ module('Integration | Identifiers - configuration', function (hooks) { assert.strictEqual(identifier.lid, 'remote:user:1', 'We receive the expected identifier for an existing record'); }); - test(`The configured generation method is used for newly created records`, async function (assert) { + test(`The configured generation method is used for newly created records`, function (assert) { let localIdInc = 9000; const generationMethod: GenerationMethod = (resource: unknown, bucket: IdentifierBucket) => { if (bucket !== 'record') { @@ -122,7 +118,7 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierGenerationMethod(generationMethod); - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const newRecord = store.createRecord('user', { firstName: 'James', username: '@cthoburn', @@ -142,8 +138,8 @@ module('Integration | Identifiers - configuration', function (hooks) { } } class TestAdapter extends Adapter { - createRecord() { - return resolve({ + override createRecord() { + return Promise.resolve({ data: { id: '1', type: 'user', @@ -160,6 +156,7 @@ module('Integration | Identifiers - configuration', function (hooks) { this.owner.register('serializer:application', TestSerializer); let updateMethodCalls = 0 as number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-const let updateCallback: (...args: any[]) => void; function updateMethod( @@ -179,8 +176,8 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierUpdateMethod(updateMethod); - const store = this.owner.lookup('service:store') as Store; - const record = store.createRecord('user', { firstName: 'Chris', username: '@runspired', age: 31 }) as DSModel; + const store = this.owner.lookup('service:store') as unknown as Store; + const record = store.createRecord('user', { firstName: 'Chris', username: '@runspired', age: 31 }) as Model; const identifier = recordIdentifierFor(record); assert.strictEqual( identifier.lid, @@ -206,8 +203,8 @@ module('Integration | Identifiers - configuration', function (hooks) { } } class TestAdapter extends Adapter { - createRecord() { - return resolve({ + override createRecord() { + return Promise.resolve({ data: { id: '1', type: 'user', @@ -224,6 +221,7 @@ module('Integration | Identifiers - configuration', function (hooks) { this.owner.register('serializer:application', TestSerializer); let updateMethodCalls = 0 as number; + // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any let updateCallback: (...args: any[]) => void; function updateMethod( @@ -243,13 +241,13 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierUpdateMethod(updateMethod); - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const record = store.createRecord('user', { id: '1', firstName: 'Chris', username: '@runspired', age: 31, - }) as DSModel; + }) as Model; const identifier = recordIdentifierFor(record); assert.strictEqual( identifier.lid, @@ -275,8 +273,8 @@ module('Integration | Identifiers - configuration', function (hooks) { } } class TestAdapter extends Adapter { - updateRecord() { - return resolve({ + override updateRecord() { + return Promise.resolve({ data: { id: '1', type: 'user', @@ -293,6 +291,7 @@ module('Integration | Identifiers - configuration', function (hooks) { this.owner.register('serializer:application', TestSerializer); let updateMethodCalls = 0 as number; + // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any let updateCallback: (...args: any[]) => void; function updateMethod( @@ -312,7 +311,7 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierUpdateMethod(updateMethod); - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const record = store.push({ data: { id: '1', @@ -323,7 +322,7 @@ module('Integration | Identifiers - configuration', function (hooks) { age: 22, }, }, - }) as DSModel; + }) as Model; const identifier = recordIdentifierFor(record); assert.strictEqual( identifier.lid, @@ -352,7 +351,8 @@ module('Integration | Identifiers - configuration', function (hooks) { }); const store = new Store(); - run(() => store.destroy()); + store.destroy(); + await settled(); assert.ok(resetMethodCalled, 'We called the reset method when the application was torn down'); }); @@ -363,8 +363,8 @@ module('Integration | Identifiers - configuration', function (hooks) { } } class TestAdapter extends Adapter { - findRecord() { - return resolve({ + override findRecord() { + return Promise.resolve({ data: { id: '1', type: 'user', @@ -385,6 +385,10 @@ module('Integration | Identifiers - configuration', function (hooks) { if (bucket !== 'record') { throw new Error('Test cannot generate an lid for a non-record'); } + if (typeof resource === 'object' && resource !== null && 'lid' in resource && typeof resource.lid === 'string') { + generateLidCalls++; + return resource.lid; + } if (typeof resource !== 'object' || resource === null || !('type' in resource)) { throw new Error('Test cannot generate an lid for a non-object'); } @@ -392,25 +396,28 @@ module('Integration | Identifiers - configuration', function (hooks) { throw new Error(`Unexpected generation of new resource identifier`); } generateLidCalls++; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${resource.type}:${resource.id}`; }); let forgetMethodCalls = 0; + let expectedIdentifier; let testMethod = (identifier) => { forgetMethodCalls++; - assert.strictEqual(expectedIdentifier, identifier, `We forgot the expected identifier ${expectedIdentifier}`); + assert.strictEqual(identifier, expectedIdentifier, `We forgot the expected identifier ${expectedIdentifier}`); }; setIdentifierForgetMethod((identifier) => { testMethod(identifier); }); - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const userByUsernamePromise = store.findRecord('user', '@runspired'); const userByIdPromise = store.findRecord('user', '1'); assert.strictEqual(generateLidCalls, 2, 'We generated two lids'); + assert.strictEqual(store.identifierCache._cache.resources.size, 2, 'We have 2 identifiers in the cache'); generateLidCalls = 0; const originalUserByUsernameIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ @@ -422,17 +429,23 @@ module('Integration | Identifiers - configuration', function (hooks) { id: '1', }); - assert.strictEqual(generateLidCalls, 0, 'We generated no new lids when we looked up the originals'); + assert.strictEqual(generateLidCalls, 2, 'We generated no new lids when we looked up the originals'); + assert.strictEqual(store.identifierCache._cache.resources.size, 2, 'We still have 2 identifiers in the cache'); generateLidCalls = 0; // we expect that the username based identifier will be abandoned expectedIdentifier = originalUserByUsernameIdentifier; - const [userByUsername, userById] = await all([userByUsernamePromise, userByIdPromise]); + const [userByUsername, userById] = await Promise.all([userByUsernamePromise, userByIdPromise]); const finalUserByUsernameIdentifier = recordIdentifierFor(userByUsername); const finalUserByIdIdentifier = recordIdentifierFor(userById); - assert.strictEqual(generateLidCalls, 0, 'We generated no new lids when we looked up the final by record'); + assert.strictEqual(generateLidCalls, 2, 'We generated no new lids when we looked up the originals'); + assert.strictEqual( + store.identifierCache._cache.resources.size, + 2, + 'We keep a back reference identifier in the cache' + ); assert.strictEqual(forgetMethodCalls, 1, 'We abandoned an identifier'); assert.notStrictEqual( @@ -451,23 +464,54 @@ module('Integration | Identifiers - configuration', function (hooks) { 'We are using the identifier by id for the result of findRecord with username' ); + const recordA = store.peekRecord({ lid: finalUserByUsernameIdentifier.lid }); + const recordB = store.peekRecord({ lid: finalUserByIdIdentifier.lid }); + const recordC = store.peekRecord({ lid: originalUserByUsernameIdentifier.lid }); + const recordD = store.peekRecord('user', '@runspired'); + const recordE = store.peekRecord('user', '1'); + + assert.strictEqual(recordA, recordB, 'We have a single record for both identifiers'); + assert.strictEqual(recordA, recordC, 'We have a single record for both identifiers'); + assert.strictEqual(recordA, recordD, 'We have a single record for both identifiers'); + assert.strictEqual(recordA, recordE, 'We have a single record for both identifiers'); + + const regeneratedIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ + lid: finalUserByUsernameIdentifier.lid, + }); + const regeneratedIdentifier2 = store.identifierCache.getOrCreateRecordIdentifier({ + id: '@runspired', + type: 'user', + }); + assert.strictEqual(regeneratedIdentifier, finalUserByUsernameIdentifier, 'We regenerate the same identifier'); + assert.strictEqual(regeneratedIdentifier2, finalUserByUsernameIdentifier, 'We regenerate the same identifier'); + + expectedIdentifier = finalUserByIdIdentifier; + store.unloadRecord(recordA); + await settled(); + assert.strictEqual( + store.identifierCache._cache.resources.size, + 0, + 'We have no identifiers or backreferences in the cache' + ); + // end test before store teardown testMethod = () => {}; }); test(`The forget method is called when a record deletion is fully persisted and the record unloaded`, async function (assert) { let forgetMethodCalls = 0; + // eslint-disable-next-line prefer-const let expectedIdentifier; setIdentifierForgetMethod((identifier) => { forgetMethodCalls++; assert.strictEqual(expectedIdentifier, identifier, `We forgot the expected identifier ${expectedIdentifier}`); }); - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const adapter = store.adapterFor('application'); adapter.deleteRecord = () => { - return resolve({ + return Promise.resolve({ data: null, }); }; @@ -480,7 +524,7 @@ module('Integration | Identifiers - configuration', function (hooks) { firstName: 'Chris', }, }, - }) as DSModel; + }) as Model; const userIdentifier = recordIdentifierFor(user); user.deleteRecord(); @@ -531,16 +575,20 @@ module('Integration | Identifiers - configuration', function (hooks) { setIdentifierForgetMethod((identifier) => { forgetMethodCalls++; - let expectedIdentifier = expectedIdentifiers.shift(); + const expectedIdentifier = expectedIdentifiers.shift(); if (expectedIdentifier) { - assert.strictEqual(expectedIdentifier, identifier, `We forgot the expected identifier ${expectedIdentifier}`); + assert.strictEqual( + expectedIdentifier, + identifier, + `We forgot the expected identifier ${expectedIdentifier.lid}` + ); } else { assert.ok(false, 'Missing expected identifier'); } }); // no retainers - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const freeWillie = store.push({ data: { type: 'user', @@ -550,7 +598,7 @@ module('Integration | Identifiers - configuration', function (hooks) { firstName: 'Chris', }, }, - }) as DSModel; + }) as Model; const freeWillieIdentifier = recordIdentifierFor(freeWillie); expectedIdentifiers.push(freeWillieIdentifier); @@ -574,7 +622,7 @@ module('Integration | Identifiers - configuration', function (hooks) { }, }, }, - }) as DSModel; + }) as Model; // the aforementioned async retainer const gatekeeper = store.push({ @@ -593,7 +641,7 @@ module('Integration | Identifiers - configuration', function (hooks) { }, }, }, - }) as DSModel; + }) as Model; // a sync reference to a record we will unload const jailhouse = store.push({ @@ -607,7 +655,7 @@ module('Integration | Identifiers - configuration', function (hooks) { retainer: { data: { type: 'retainer', id: '1' } }, }, }, - }) as DSModel; + }) as Model; const jailBirdIdentifier = recordIdentifierFor(jailBird); const gatekeeperIdentifier = recordIdentifierFor(gatekeeper); diff --git a/tests/main/tests/integration/identifiers/lid-reflection-test.ts b/tests/main/tests/integration/identifiers/lid-reflection-test.ts index 7d24140a268..1fbc63cab0e 100644 --- a/tests/main/tests/integration/identifiers/lid-reflection-test.ts +++ b/tests/main/tests/integration/identifiers/lid-reflection-test.ts @@ -1,35 +1,39 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { defer, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import type { ManyArray } from '@ember-data/model'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; +import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; module('Integration | Identifiers - lid reflection', function (hooks: NestedHooks) { setupTest(hooks); - let store; + + class User extends Model { + @attr declare name: string; + @attr declare age: number; + + [Type] = 'user' as const; + } hooks.beforeEach(function () { const { owner } = this; - class User extends Model { - @attr declare name: string; - @attr declare age: number; - } - owner.register('model:user', User); - store = owner.lookup('service:store'); }); - test(`We can access the lid when serializing a record`, async function (assert: Assert) { + test(`We can access the lid when serializing a record`, function (assert: Assert) { class TestSerializer extends EmberObject { - serialize(snapshot: Snapshot) { - const identifier = snapshot.identifier; + serialize(snapshot: Snapshot) { + // TODO should snapshots have direct access to the identifier? + const identifier = recordIdentifierFor(snapshot.record); return { type: snapshot.modelName, id: snapshot.id, @@ -42,16 +46,18 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook } this.owner.register('serializer:application', TestSerializer); - const record = store.createRecord('user', { name: 'Chris' }); + const store = this.owner.lookup('service:store') as Store; + const record = store.createRecord('user', { name: 'Chris' }); const identifier = recordIdentifierFor(record); - const serialized = record.serialize(); + const serialized = record.serialize() as Record; assert.notStrictEqual(identifier.lid, null, 'We have an lid'); assert.strictEqual(serialized.lid, identifier.lid, 'We have the right lid'); }); - test(`A newly created record can receive a payload by lid (no save ever called)`, async function (assert: Assert) { - const record = store.createRecord('user', { name: 'Chris' }); + test(`A newly created record can receive a payload by lid (no save ever called)`, function (assert: Assert) { + const store = this.owner.lookup('service:store') as Store; + const record = store.createRecord('user', { name: 'Chris' }); const identifier = recordIdentifierFor(record); assert.notStrictEqual(identifier.lid, null, 'We have an lid'); @@ -77,22 +83,22 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook }); test(`A newly created record can receive a payload by lid (after save, before Adapter.createRecord resolves)`, async function (assert: Assert) { - const adapterPromise = defer(); - const beganSavePromise = defer(); + const adapterPromise = createDeferred(); + const beganSavePromise = createDeferred(); class TestSerializer extends EmberObject { - normalizeResponse(_, __, payload) { + normalizeResponse(_, __, payload: Record) { return payload; } } class TestAdapter extends Adapter { - createRecord(store, ModelClass, snapshot) { - beganSavePromise.resolve(); + override createRecord(store, ModelClass, snapshot: Snapshot) { + beganSavePromise.resolve(void 0); return adapterPromise.promise.then(() => { return { data: { type: 'user', id: '1', - lid: snapshot.identifier.lid, + lid: recordIdentifierFor(snapshot.record).lid, attributes: { name: '@runspired', }, @@ -104,7 +110,8 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook this.owner.register('serializer:application', TestSerializer); this.owner.register('adapter:application', TestAdapter); - const record = store.createRecord('user', { name: 'Chris' }); + const store = this.owner.lookup('service:store') as Store; + const record = store.createRecord('user', { name: 'Chris' }); const identifier = recordIdentifierFor(record); assert.notStrictEqual(identifier.lid, null, 'We have an lid'); @@ -135,7 +142,7 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook assert.strictEqual(record.name, 'Chris', 'We use the in-flight name, rollback has no effect'); - adapterPromise.resolve(); + adapterPromise.resolve(void 0); await savePromise; assert.strictEqual(record.name, '@runspired', 'After we finish we use the most recent clean name'); @@ -145,27 +152,32 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook class Ingredient extends Model { @attr name; @belongsTo('cake', { async: true, inverse: null }) cake; + + [Type] = 'ingredient' as const; } class Cake extends Model { @attr name; - @hasMany('ingredient', { inverse: null, async: false }) ingredients; + @hasMany('ingredient', { inverse: null, async: false }) declare ingredients: ManyArray; + + [Type] = 'cake' as const; } this.owner.register('model:ingredient', Ingredient); this.owner.register('model:cake', Cake); class TestSerializer extends EmberObject { - normalizeResponse(_, __, payload) { + normalizeResponse(_, __, payload: Record) { return payload; } } class TestAdapter extends Adapter { - createRecord(store, ModelClass, snapshot) { - const cakeLid = snapshot.identifier.lid; - const ingredientLid = recordIdentifierFor(snapshot.record!.ingredients.at(0)).lid; - return resolve({ + override createRecord(store, ModelClass, snapshot: Snapshot) { + const record = snapshot.record as Cake; + const cakeLid = recordIdentifierFor(record).lid; + const ingredientLid = recordIdentifierFor(record.ingredients.at(0)).lid; + return Promise.resolve({ data: { type: 'cake', id: '1', @@ -210,8 +222,9 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook this.owner.register('serializer:application', TestSerializer); this.owner.register('adapter:application', TestAdapter); - const cheese = store.createRecord('ingredient', { name: 'Cheese' }); - const cake = store.createRecord('cake', { name: 'Cheesecake', ingredients: [cheese] }); + const store = this.owner.lookup('service:store') as Store; + const cheese = store.createRecord('ingredient', { name: 'Cheese' }); + const cake = store.createRecord('cake', { name: 'Cheesecake', ingredients: [cheese] }); // Consume ids before save() to check for update errors assert.strictEqual(cake.id, null, 'cake id is initially null'); @@ -220,7 +233,7 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook await cake.save(); assert.deepEqual(cake.hasMany('ingredients').ids(), ['2']); - assert.strictEqual(cake.ingredients.at(0).name, 'Cheese'); + assert.strictEqual(cake.ingredients.at(0)?.name, 'Cheese'); assert.strictEqual(cake.id, '1', 'cake has the correct id'); assert.strictEqual(cheese.id, '2', 'cheese has the correct id'); @@ -229,26 +242,31 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook test('belongsTo() has correct state after .save() on a newly created record with sideposted child record when lid is provided in the response payload', async function (assert: Assert) { class Topping extends Model { @attr name; + + [Type] = 'topping' as const; } class Cake extends Model { @attr name; - @belongsTo('topping', { inverse: null, async: false }) topping; + @belongsTo('topping', { inverse: null, async: false }) declare topping: Topping; + + [Type] = 'cake' as const; } this.owner.register('model:topping', Topping); this.owner.register('model:cake', Cake); class TestSerializer extends EmberObject { - normalizeResponse(_, __, payload) { + normalizeResponse(_, __, payload: unknown) { return payload; } } class TestAdapter extends Adapter { - createRecord(store, ModelClass, snapshot) { - const lid = recordIdentifierFor(snapshot.record!.topping).lid; - return resolve({ + override createRecord(store, ModelClass, snapshot: Snapshot) { + const record = snapshot.record as Cake; + const lid = recordIdentifierFor(record.topping).lid; + return Promise.resolve({ data: { type: 'cake', id: '1', @@ -289,8 +307,9 @@ module('Integration | Identifiers - lid reflection', function (hooks: NestedHook this.owner.register('serializer:application', TestSerializer); this.owner.register('adapter:application', TestAdapter); - const cheese = store.createRecord('topping', { name: 'Cheese' }); - const cake = store.createRecord('cake', { name: 'Cheesecake', topping: cheese }); + const store = this.owner.lookup('service:store') as Store; + const cheese = store.createRecord('topping', { name: 'Cheese' }); + const cake = store.createRecord('cake', { name: 'Cheesecake', topping: cheese }); await cake.save(); diff --git a/tests/main/tests/integration/identifiers/new-records-test.ts b/tests/main/tests/integration/identifiers/new-records-test.ts index 15dad45af39..8da8928dbe0 100644 --- a/tests/main/tests/integration/identifiers/new-records-test.ts +++ b/tests/main/tests/integration/identifiers/new-records-test.ts @@ -20,7 +20,7 @@ module('Integration | Identifiers - creating new records', function (hooks) { store = owner.lookup('service:store'); }); - test(`We can peek before create`, async function (assert) { + test(`We can peek before create`, function (assert) { let record = store.peekRecord('user', '1'); assert.strictEqual(record, null, 'peekRecord returns null'); diff --git a/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts b/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts index 9fdba4ebe92..7bb058145bc 100644 --- a/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts +++ b/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts @@ -1,15 +1,45 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; +import type { AsyncBelongsTo, AsyncHasMany } from '@ember-data/model'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; type RID = { type: string; id: string }; +class Car extends Model { + @attr() + declare color: string; + + declare [Type]: 'car' | 'ferrari' | 'bmw'; +} + +class Ferrari extends Car { + [Type] = 'ferrari' as const; +} +class Bmw extends Car { + [Type] = 'bmw' as const; +} + +class Dealership extends Model { + @attr + declare name: string; + + @belongsTo('car', { polymorphic: true, async: true, inverse: null }) + declare bestCar: AsyncBelongsTo; + + @hasMany('car', { polymorphic: true, async: true, inverse: null }) + declare allCars: AsyncHasMany; + + declare [Type]: 'dealership'; +} + module('Integration | Identifiers - single-table-inheritance polymorphic scenarios', function (hooks) { /* In single-table polymorphism, each polymorphic type shares a common primaryKey field. @@ -26,52 +56,35 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari */ setupTest(hooks); - module('single-table', function (hooks) { - let store; + module('single-table', function (innerHooks) { + let store: Store; class TestSerializer extends EmberObject { - normalizeResponse(_, __, payload) { + normalizeResponse(_, __, payload: unknown) { return payload; } } - hooks.beforeEach(function () { + innerHooks.beforeEach(function () { const { owner } = this; - class Car extends Model { - @attr() - declare color: string; - } - - class Ferrari extends Car {} - class Bmw extends Car {} - - class Dealership extends Model { - @attr() - declare name: string; - @belongsTo('car', { polymorphic: true, async: true, inverse: null }) - declare bestCar; - @hasMany('car', { polymorphic: true, async: true, inverse: null }) - declare allCars; - } - owner.register('serializer:application', TestSerializer); owner.register('model:car', Car); owner.register('model:ferrari', Ferrari); owner.register('model:bmw', Bmw); owner.register('model:dealership', Dealership); - store = owner.lookup('service:store'); + store = owner.lookup('service:store') as Store; }); test(`Identity of polymorphic relations can change type on first load`, async function (assert) { const { owner } = this; class TestAdapter extends Adapter { - shouldBackgroundReloadRecord() { + override shouldBackgroundReloadRecord() { return false; } - findRecord(_, __, id) { - return resolve({ + override findRecord(_, __, id: string) { + return Promise.resolve({ data: { id, type: 'ferrari', @@ -84,11 +97,22 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari } owner.register('adapter:application', TestAdapter); - const foundFerrari = await store.findRecord('car', '1'); - assert.strictEqual(foundFerrari.constructor.modelName, 'ferrari', 'We found the right type'); + const foundFerrari = await store.findRecord('car', '1'); + assert.strictEqual( + (foundFerrari.constructor as unknown as { modelName: string }).modelName, + 'ferrari', + 'We found the right type' + ); + assert.strictEqual(recordIdentifierFor(foundFerrari).type, 'ferrari', 'We ended with the correct type'); - const cachedFerrari = await store.peekRecord('ferrari', '1'); - assert.strictEqual(cachedFerrari.constructor.modelName, 'ferrari', 'We cached the right type'); + const cachedFerrari = store.peekRecord('ferrari', '1'); + assert.strictEqual( + (cachedFerrari?.constructor as unknown as { modelName: string }).modelName, + 'ferrari', + 'We cached the right type' + ); + assert.strictEqual(recordIdentifierFor(cachedFerrari).type, 'ferrari', 'We ended with the correct type'); + assert.strictEqual(foundFerrari, cachedFerrari, 'We have the same car'); }); test(`Identity of polymorphic relations can change type when in cache`, async function (assert) { @@ -101,12 +125,12 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari { id: '2', type: 'car' }, ]; class TestAdapter extends Adapter { - shouldBackgroundReloadRecord() { + override shouldBackgroundReloadRecord() { return false; } - findRecord(_, { modelName: type }, id) { + override findRecord(_, { modelName: type }, id: string) { if (type === 'dealership') { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dealership', @@ -130,7 +154,7 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari requests.push({ type, id }); // return the polymorphic type instead of 'car'; type = id === '1' ? 'ferrari' : 'bmw'; - return resolve({ + return Promise.resolve({ data: { id, type, @@ -142,11 +166,15 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari } } owner.register('adapter:application', TestAdapter); - const topRecord = await store.findRecord('dealership', '1'); + const topRecord = await store.findRecord('dealership', '1'); const relation = await topRecord.bestCar; - assert.strictEqual(relation.id, '1', 'We found the right id'); - assert.strictEqual(relation.constructor.modelName, 'ferrari', 'We found the right type'); + assert.strictEqual(relation?.id, '1', 'We found the right id'); + assert.strictEqual( + (relation?.constructor as unknown as { modelName: string }).modelName, + 'ferrari', + 'We found the right type' + ); const foundFerrari = await store.findRecord('car', '1'); assert.strictEqual(relation, foundFerrari, 'We found the ferrari by finding car 1'); @@ -154,7 +182,7 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari const allCars = await topRecord.allCars; assert.deepEqual( allCars.map((c) => { - return { id: c.id, type: c.constructor.modelName }; + return { id: c.id, type: (c.constructor as unknown as { modelName: string }).modelName }; }), [ { id: '1', type: 'ferrari' }, diff --git a/tests/main/tests/integration/identifiers/record-identifier-for-test.ts b/tests/main/tests/integration/identifiers/record-identifier-for-test.ts index 3dd56aa4663..f52e5641e6e 100644 --- a/tests/main/tests/integration/identifiers/record-identifier-for-test.ts +++ b/tests/main/tests/integration/identifiers/record-identifier-for-test.ts @@ -1,31 +1,31 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr } from '@ember-data/model'; +import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; +class User extends Model { + @attr() name; +} + module('Integration | Identifiers - recordIdentifierFor', function (hooks) { setupTest(hooks); - let store; + let store: Store; hooks.beforeEach(function () { const { owner } = this; - class User extends Model { - @attr() name; - } - owner.register('model:user', User); - store = owner.lookup('service:store'); + store = owner.lookup('service:store') as Store; }); - test(`It works for newly created records`, async function (assert) { - const record = store.createRecord('user', { name: 'Chris' }); + test(`It works for newly created records`, function (assert) { + const record = store.createRecord('user', { name: 'Chris' }) as User; assert.strictEqual(record.name, 'Chris', 'We created a record'); const identifier = recordIdentifierFor(record); @@ -36,8 +36,8 @@ module('Integration | Identifiers - recordIdentifierFor', function (hooks) { test(`Saving newly created records updates the associated id on the identifier`, async function (assert) { class TestAdapter extends Adapter { - createRecord() { - return resolve({ + override createRecord() { + return Promise.resolve({ data: { type: 'user', id: '1', @@ -49,13 +49,13 @@ module('Integration | Identifiers - recordIdentifierFor', function (hooks) { } } class TestSerializer extends EmberObject { - normalizeResponse(_, __, payload) { + normalizeResponse(_, __, payload: unknown) { return payload; } } this.owner.register('adapter:application', TestAdapter); this.owner.register('serializer:application', TestSerializer); - const record = store.createRecord('user', { name: 'Chris' }); + const record = store.createRecord('user', { name: 'Chris' }) as User; assert.strictEqual(record.name, 'Chris', 'We created a record'); const identifier = recordIdentifierFor(record); @@ -72,7 +72,7 @@ module('Integration | Identifiers - recordIdentifierFor', function (hooks) { assert.ok(typeof identifier.lid === 'string' && identifier.lid.length > 0, 'We have an identifier with an lid'); }); - test(`It works for existing records`, async function (assert) { + test(`It works for existing records`, function (assert) { const record = store.push({ data: { type: 'user', @@ -81,7 +81,7 @@ module('Integration | Identifiers - recordIdentifierFor', function (hooks) { name: 'Chris', }, }, - }); + }) as User; assert.strictEqual(record.name, 'Chris', 'We created a record'); const identifier = recordIdentifierFor(record); diff --git a/tests/main/tests/integration/identifiers/scenarios-test.ts b/tests/main/tests/integration/identifiers/scenarios-test.ts index 672d1194a86..216dfe14966 100644 --- a/tests/main/tests/integration/identifiers/scenarios-test.ts +++ b/tests/main/tests/integration/identifiers/scenarios-test.ts @@ -1,13 +1,12 @@ import EmberObject, { set } from '@ember/object'; import { module, test } from 'qunit'; -import { all, resolve } from 'rsvp'; +import type Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr } from '@ember-data/model'; -import type Store from '@ember-data/store'; import { recordIdentifierFor, setIdentifierForgetMethod, @@ -15,17 +14,13 @@ import { setIdentifierResetMethod, setIdentifierUpdateMethod, } from '@ember-data/store'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import type { - GenerationMethod, - IdentifierBucket, - ResourceData, - StableIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import type { ConfidentDict } from '@ember-data/types/q/utils'; - -function isNonEmptyString(str: any): str is string { +import type { IdentifierBucket, StableIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ExistingResourceObject, ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; + +type ResourceData = ResourceIdentifierObject | ExistingResourceObject; +type GenerationMethod = Parameters[0]; + +function isNonEmptyString(str: unknown): str is string { return typeof str === 'string' && str.length > 0; } @@ -36,12 +31,12 @@ function isResourceData(resource: object): resource is ResourceData { module('Integration | Identifiers - scenarios', function (hooks) { setupTest(hooks); - module('Secondary Cache based on an attribute', function (hooks) { + module('Secondary Cache based on an attribute', function (innerHooks) { let calls; let isQuery = false; let secondaryCache: { - id: ConfidentDict; - username: ConfidentDict; + id: { [key: string]: string }; + username: { [key: string]: string }; }; class TestSerializer extends EmberObject { normalizeResponse(_, __, payload) { @@ -49,15 +44,15 @@ module('Integration | Identifiers - scenarios', function (hooks) { } } class TestAdapter extends Adapter { - shouldBackgroundReloadRecord() { + override shouldBackgroundReloadRecord() { return false; } - findRecord() { + override findRecord() { if (isQuery !== true) { calls.findRecord++; } isQuery = false; - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'user', @@ -69,14 +64,14 @@ module('Integration | Identifiers - scenarios', function (hooks) { }, }); } - queryRecord() { + override queryRecord() { calls.queryRecord++; isQuery = true; return this.findRecord(); } } - hooks.beforeEach(function () { + innerHooks.beforeEach(function () { const { owner } = this; class User extends Model { @@ -119,7 +114,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { } let lid = resource.lid; - let username = 'attributes' in resource && resource.attributes && resource.attributes.username; + const username = 'attributes' in resource && resource.attributes && resource.attributes.username; // try the username cache if (!lid && isNonEmptyString(username)) { @@ -168,7 +163,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { setIdentifierGenerationMethod(generationMethod); }); - hooks.afterEach(function () { + innerHooks.afterEach(function () { setIdentifierGenerationMethod(null); setIdentifierResetMethod(null); setIdentifierUpdateMethod(null); @@ -176,11 +171,11 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord id then queryRecord with username`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordById = await store.findRecord('user', '1'); const identifierById = recordIdentifierFor(recordById); const recordByUsername = await store.queryRecord('user', { username: '@runspired' }); - const identifierByUsername = recordIdentifierFor(recordByUsername!); + const identifierByUsername = recordIdentifierFor(recordByUsername); assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical'); assert.strictEqual(recordById, recordByUsername, 'The records should be identical'); @@ -188,7 +183,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -197,9 +192,9 @@ module('Integration | Identifiers - scenarios', function (hooks) { ); }); test(`queryRecord with username then findRecord with id`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordByUsername = await store.queryRecord('user', { username: '@runspired' }); - const identifierByUsername = recordIdentifierFor(recordByUsername!); + const identifierByUsername = recordIdentifierFor(recordByUsername); const recordById = await store.findRecord('user', '1'); const identifierById = recordIdentifierFor(recordById); @@ -209,7 +204,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -218,7 +213,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { ); }); test(`queryRecord with username and findRecord with id in parallel`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordByUsernamePromise1 = store.queryRecord('user', { username: '@runspired' }); const recordByIdPromise = store.findRecord('user', '1'); const recordByUsernamePromise2 = store.queryRecord('user', { username: '@runspired' }); @@ -228,8 +223,8 @@ module('Integration | Identifiers - scenarios', function (hooks) { const recordByUsername2 = await recordByUsernamePromise2; const identifierById = recordIdentifierFor(recordById); - const identifierByUsername1 = recordIdentifierFor(recordByUsername1!); - const identifierByUsername2 = recordIdentifierFor(recordByUsername2!); + const identifierByUsername1 = recordIdentifierFor(recordByUsername1); + const identifierByUsername2 = recordIdentifierFor(recordByUsername2); assert.strictEqual(identifierById, identifierByUsername1, 'The identifiers should be identical'); assert.strictEqual(identifierById, identifierByUsername2, 'The identifiers should be identical'); @@ -239,7 +234,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 2, 'We made two calls to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -249,25 +244,25 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); }); - module('Secondary Cache using an attribute as an alternate id', function (hooks) { + module('Secondary Cache using an attribute as an alternate id', function (innerHooks) { let calls; let isQuery = false; - let secondaryCache: ConfidentDict; + let secondaryCache: { [key: string]: string }; class TestSerializer extends EmberObject { normalizeResponse(_, __, payload) { return payload; } } class TestAdapter extends Adapter { - shouldBackgroundReloadRecord() { + override shouldBackgroundReloadRecord() { return false; } - findRecord() { + override findRecord() { if (isQuery !== true) { calls.findRecord++; } isQuery = false; - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'user', @@ -279,24 +274,24 @@ module('Integration | Identifiers - scenarios', function (hooks) { }, }); } - queryRecord() { + override queryRecord() { calls.queryRecord++; isQuery = true; return this.findRecord(); } } - hooks.beforeEach(function () { - const { owner } = this; + class User extends Model { + @attr() + declare firstName: string; + @attr() + declare username: string; + @attr() + declare age: number; + } - class User extends Model { - @attr() - declare firstName: string; - @attr() - declare username: string; - @attr() - declare age: number; - } + innerHooks.beforeEach(function () { + const { owner } = this; owner.register('adapter:application', TestAdapter); owner.register('serializer:application', TestSerializer); @@ -316,7 +311,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { return `local:user:${localIdInc++}`; } let lid = resource.lid; - let username = 'attributes' in resource && resource.attributes && resource.attributes.username; + const username = 'attributes' in resource && resource.attributes && resource.attributes.username; // try the username cache if (!lid && isNonEmptyString(username)) { @@ -393,6 +388,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { (resource as ResourceData).lid = identifier.lid; lidForUser(resource as ResourceData); } else { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled update for ${bucket}`); } }; @@ -401,7 +397,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { setIdentifierUpdateMethod(updateMethod); }); - hooks.afterEach(function () { + innerHooks.afterEach(function () { setIdentifierGenerationMethod(null); setIdentifierResetMethod(null); setIdentifierUpdateMethod(null); @@ -409,8 +405,8 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord by id then by username as id`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const recordById = await store.findRecord('user', '1'); + const store = this.owner.lookup('service:store') as unknown as Store; + const recordById = (await store.findRecord('user', '1')) as User; const identifierById = recordIdentifierFor(recordById); const recordByUsername = await store.findRecord('user', '@runspired'); const identifierByUsername = recordIdentifierFor(recordByUsername); @@ -422,7 +418,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -432,10 +428,10 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord by username as id then by id`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordByUsername = await store.findRecord('user', '@runspired'); const identifierByUsername = recordIdentifierFor(recordByUsername); - const recordById = await store.findRecord('user', '1'); + const recordById = (await store.findRecord('user', '1')) as User; const identifierById = recordIdentifierFor(recordById); assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical'); @@ -445,7 +441,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -455,11 +451,14 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord username and findRecord id in parallel`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordByUsernamePromise = store.findRecord('user', '@runspired'); const recordByIdPromise = store.findRecord('user', '1'); - const [recordByUsername, recordById] = await all([recordByUsernamePromise, recordByIdPromise]); + const [recordByUsername, recordById] = (await Promise.all([recordByUsernamePromise, recordByIdPromise])) as [ + User, + User, + ]; const identifierByUsername = recordIdentifierFor(recordByUsername); const identifierById = recordIdentifierFor(recordById); @@ -471,16 +470,18 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; - const lids = [...lidCache.values()]; - assert.strictEqual( - lidCache.size, - 1, - `We only have the lid '${identifierByUsername.lid}' in ['${lids.join("', '")}']` + const lidCache = store.identifierCache._cache.resources; + assert.strictEqual(lidCache.size, 2, `We should have both lids in the cache still since one is a backreference`); + assert.deepEqual( + [...lidCache.keys()], + ['remote:user:1:9001', 'remote:user:@runspired:9000'], + 'We have the expected keys' ); + const lids = [...lidCache.values()]; + assert.arrayStrictEquals(lids, [identifierById, identifierById], 'We have the expected values'); // ensure we still use secondary caching for @runspired post-merging of the identifiers - const recordByUsernameAgain = await store.findRecord('user', '@runspired'); + const recordByUsernameAgain = (await store.findRecord('user', '@runspired')) as User; const identifier = recordIdentifierFor(recordByUsernameAgain); assert.strictEqual(identifierById, identifier, 'The identifiers should be identical'); @@ -491,10 +492,10 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord by username and again`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const recordByUsername = await store.findRecord('user', '@runspired'); + const store = this.owner.lookup('service:store') as unknown as Store; + const recordByUsername = (await store.findRecord('user', '@runspired')) as User; const identifierByUsername = recordIdentifierFor(recordByUsername); - const recordByUsername2 = await store.findRecord('user', '@runspired'); + const recordByUsername2 = (await store.findRecord('user', '@runspired')) as User; const identifierByUsername2 = recordIdentifierFor(recordByUsername2); assert.strictEqual(identifierByUsername2, identifierByUsername, 'The identifiers should be identical'); @@ -504,7 +505,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierByUsername.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -543,10 +544,10 @@ module('Integration | Identifiers - scenarios', function (hooks) { the "id" position. */ test(`findRecord by username and reload`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; - const recordByUsername = await store.findRecord('user', '@runspired'); + const store = this.owner.lookup('service:store') as unknown as Store; + const recordByUsername = (await store.findRecord('user', '@runspired')) as User; const identifierByUsername = recordIdentifierFor(recordByUsername); - const recordByUsername2 = await store.findRecord('user', '@runspired', { reload: true }); + const recordByUsername2 = (await store.findRecord('user', '@runspired', { reload: true })) as User; const identifierByUsername2 = recordIdentifierFor(recordByUsername2); assert.strictEqual(identifierByUsername2, identifierByUsername, 'The identifiers should be identical'); @@ -556,7 +557,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierByUsername.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -566,7 +567,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`push id then findRecord username`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordById = store.push({ data: { type: 'user', @@ -577,9 +578,9 @@ module('Integration | Identifiers - scenarios', function (hooks) { age: 31, }, }, - }); + }) as User; const identifierById = recordIdentifierFor(recordById); - const recordByUsername = await store.findRecord('user', '@runspired'); + const recordByUsername = (await store.findRecord('user', '@runspired')) as User; const identifierByUsername = recordIdentifierFor(recordByUsername); assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical'); @@ -589,7 +590,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -599,7 +600,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`findRecord username then push id`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const recordByUsername = await store.findRecord('user', '@runspired'); const identifierByUsername = recordIdentifierFor(recordByUsername); const recordById = store.push({ @@ -612,7 +613,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { age: 31, }, }, - }); + }) as User; const identifierById = recordIdentifierFor(recordById); assert.strictEqual(identifierById, identifierByUsername, 'The identifiers should be identical'); @@ -621,7 +622,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -631,7 +632,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { }); test(`secondary-key mutation`, async function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as unknown as Store; const adapter = store.adapterFor('application'); let hasSaved = false; @@ -639,7 +640,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { if (hasSaved && id === '@runspired') { throw new Error(`No record found for the username @runspired`); } - return resolve({ + return Promise.resolve({ data: { type: 'user', id: '1', @@ -652,7 +653,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { adapter.updateRecord = () => { hasSaved = true; - return resolve({ + return Promise.resolve({ data: { type: 'user', id: '1', @@ -678,7 +679,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { } } - const user = (await store.findRecord('user', '@runspired')) as DSModel; + const user = (await store.findRecord('user', '@runspired')) as Model; const identifier = recordIdentifierFor(user); set(user, 'username', '@cthoburn'); diff --git a/tests/main/tests/integration/inverse-test.js b/tests/main/tests/integration/inverse-test.js index 0326e1ee930..217ff318a38 100644 --- a/tests/main/tests/integration/inverse-test.js +++ b/tests/main/tests/integration/inverse-test.js @@ -2,9 +2,9 @@ import { module } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { DEBUG } from '@ember-data/env'; 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 () { @@ -17,7 +17,7 @@ module('integration/inverse-test - inverseFor', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; store = owner.lookup('service:store'); }); @@ -52,18 +52,17 @@ module('integration/inverse-test - inverseFor', function (hooks) { } } - let { owner } = this; + const { owner } = this; owner.register('model:user', User); owner.register('model:job', Job); - let job = store.modelFor('job'); - let user = store.modelFor('user'); - let inverseDefinition = job.inverseFor('user', store); + const job = store.modelFor('job'); + const inverseDefinition = job.inverseFor('user', store); assert.deepEqual( inverseDefinition, { - type: user, + type: 'user', name: 'job', kind: 'belongsTo', options: { @@ -112,17 +111,17 @@ module('integration/inverse-test - inverseFor', function (hooks) { } } - let { owner } = this; + const { owner } = this; owner.register('model:user', User); owner.register('model:job', Job); - let job = store.modelFor('job'); - let user = store.modelFor('user'); + const job = store.modelFor('job'); + const user = store.modelFor('user'); assert.deepEqual( job.inverseFor('owner', store), { - type: user, //the model's type + type: 'user', //the model's type name: 'previousJob', //the models relationship key kind: 'belongsTo', options: { @@ -135,7 +134,7 @@ module('integration/inverse-test - inverseFor', function (hooks) { assert.deepEqual( user.inverseFor('previousJob', store), { - type: job, //the model's type + type: 'job', //the model's type name: 'owner', //the models relationship key kind: 'belongsTo', options: { @@ -179,11 +178,11 @@ module('integration/inverse-test - inverseFor', function (hooks) { } } - let { owner } = this; + const { owner } = this; owner.register('model:user', User); owner.register('model:job', Job); - let user = store.modelFor('user'); + const user = store.modelFor('user'); assert.strictEqual(user.inverseFor('job', store), null, 'There is no inverse'); } ); @@ -222,14 +221,14 @@ module('integration/inverse-test - inverseFor', function (hooks) { return stringify('job'); } } - let { owner } = this; + const { owner } = this; owner.register('model:user', User); owner.register('model:job', Job); - let user = store.modelFor('user'); + const user = store.modelFor('user'); assert.expectAssertion(() => { user.inverseFor('job', store); - }, /Assertion Failed: You defined the 'job' relationship on model:user, but you defined the inverse relationships of type model:job multiple times/i); + }, /You defined the 'job' relationship on model:user, but you defined the inverse relationships of type model:job multiple times/i); } ); } @@ -265,13 +264,13 @@ module('integration/inverse-test - inverseFor', function (hooks) { return stringify('job'); } } - let { owner } = this; + const { owner } = this; owner.register('model:user', User); owner.register('model:job', Job); - let job = store.modelFor('job'); + const job = store.modelFor('job'); - let inverseForUser = job.inverseFor('user', store); + const inverseForUser = job.inverseFor('user', store); job.findInverseFor = function () { assert.ok(false, 'Find is not called anymore'); }; @@ -294,7 +293,7 @@ module('integration/inverse-test - inverseFor', function (hooks) { } } - let { owner } = this; + const { owner } = this; owner.register('model:reflexive-model', ReflexiveModel); //Maybe store is evaluated lazily, so we need this :( diff --git a/tests/main/tests/integration/legacy-compat/find-all-test.ts b/tests/main/tests/integration/legacy-compat/find-all-test.ts index b50dc3e2cba..b6891f5b520 100644 --- a/tests/main/tests/integration/legacy-compat/find-all-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-all-test.ts @@ -2,13 +2,15 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import type { CompatStore } from '@ember-data/legacy-compat'; import { findAll } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; -import type Store from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; type FindAllBuilderOptions = Exclude[1], undefined>; class Post extends Model { + [Type] = 'post' as const; @attr declare name: string; } @@ -40,8 +42,8 @@ module('Integration - legacy-compat/builders/findAll', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const { content: results } = await store.request(findAll('post')); + const store = this.owner.lookup('service:store') as CompatStore; + const { content: results } = await store.request(findAll('post')); assert.strictEqual(results.length, 1, 'post was found'); assert.strictEqual(results[0].id, '1', 'post has correct id'); @@ -50,7 +52,7 @@ module('Integration - legacy-compat/builders/findAll', function (hooks) { }); test('findAll', function (assert) { - const result = findAll('post'); + const result = findAll('post'); assert.deepEqual( result, { diff --git a/tests/main/tests/integration/legacy-compat/find-record-test.ts b/tests/main/tests/integration/legacy-compat/find-record-test.ts index d867647518b..bd2215c28f6 100644 --- a/tests/main/tests/integration/legacy-compat/find-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-record-test.ts @@ -2,13 +2,16 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import type { CompatStore } from '@ember-data/legacy-compat'; import { findRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; -import type Store from '@ember-data/store'; +import type { FindRecordOptions } from '@ember-data/store/types'; +import { Type } from '@warp-drive/core-types/symbols'; type FindRecordBuilderOptions = Exclude[1], undefined>; class Post extends Model { + [Type] = 'post' as const; @attr declare name: string; } @@ -38,8 +41,8 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const { content: post } = await store.request(findRecord('post', '1')); + const store = this.owner.lookup('service:store') as CompatStore; + const { content: post } = await store.request(findRecord('post', '1')); assert.strictEqual(post.id, '1', 'post has correct id'); assert.strictEqual(post.name, 'Krystan rules, you drool', 'post has correct name'); @@ -47,7 +50,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { }); test('findRecord by type+id', function (assert) { - const result = findRecord('post', '1'); + const result = findRecord('post', '1'); assert.deepEqual( result, { @@ -69,7 +72,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { include: 'author,comments', adapterOptions: {}, }; - const result = findRecord('post', '1', options); + const result = findRecord('post', '1', options); assert.deepEqual( result, { @@ -85,17 +88,18 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { }); test('findRecord by type+id with invalid options', async function (assert) { - const invalidOptions = { + // Type hacks to ensure we're notified if we add new FindRecordOptions that aren't valid FindRecordBuilderOptions + const invalidOptions: Omit, keyof FindRecordBuilderOptions> = { preload: {}, }; await assert.expectAssertion(() => { // @ts-expect-error TS knows the options are invalid - findRecord('post', '1', invalidOptions); - }, 'Assertion Failed: findRecord builder does not support options.preload'); + findRecord('post', '1', invalidOptions); + }, 'findRecord builder does not support options.preload'); }); test('findRecord by identifier', function (assert) { - const result = findRecord({ type: 'post', id: '1' }); + const result = findRecord({ type: 'post', id: '1' }); assert.deepEqual( result, { @@ -117,7 +121,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { include: 'author,comments', adapterOptions: {}, }; - const result = findRecord({ type: 'post', id: '1' }, options); + const result = findRecord({ type: 'post', id: '1' }, options); assert.deepEqual( result, { @@ -134,12 +138,12 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { test('findRecord by identifier with invalid options', async function (assert) { // Type hacks to ensure we're notified if we add new FindRecordOptions that aren't valid FindRecordBuilderOptions - const invalidOptions = { + const invalidOptions: Omit, keyof FindRecordBuilderOptions> = { preload: {}, }; await assert.expectAssertion(() => { // @ts-expect-error TS knows the options are invalid - findRecord({ type: 'post', id: '1' }, invalidOptions); - }, 'Assertion Failed: findRecord builder does not support options.preload'); + findRecord({ type: 'post', id: '1' }, invalidOptions); + }, 'findRecord builder does not support options.preload'); }); }); diff --git a/tests/main/tests/integration/legacy-compat/query-test.ts b/tests/main/tests/integration/legacy-compat/query-test.ts index c87a32c6962..80794c006a3 100644 --- a/tests/main/tests/integration/legacy-compat/query-test.ts +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -2,14 +2,16 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import type { CompatStore } from '@ember-data/legacy-compat'; import { query, queryRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; -import type Store from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; type QueryBuilderOptions = Exclude[2], undefined>; type QueryRecordBuilderOptions = Exclude[2], undefined>; class Post extends Model { + [Type] = 'post' as const; @attr declare name: string; } @@ -42,8 +44,8 @@ module('Integration - legacy-compat/builders/query', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const { content: results } = await store.request(query('post', { id: '1' })); + const store = this.owner.lookup('service:store') as CompatStore; + const { content: results } = await store.request(query('post', { id: '1' })); assert.strictEqual(results.length, 1, 'post was found'); assert.strictEqual(results[0].id, '1', 'post has correct id'); @@ -52,7 +54,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { }); test('query', function (assert) { - const result = query('post', { id: '1' }); + const result = query('post', { id: '1' }); assert.deepEqual( result, { @@ -73,7 +75,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { whatever: true, adapterOptions: {}, }; - const result = query('post', { id: '1' }, options); + const result = query('post', { id: '1' }, options); assert.deepEqual( result, { @@ -114,8 +116,8 @@ module('Integration - legacy-compat/builders/query', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const { content: post } = await store.request(queryRecord('post', { id: '1' })); + const store = this.owner.lookup('service:store') as CompatStore; + const { content: post } = await store.request(queryRecord('post', { id: '1' })); assert.strictEqual(post?.id, '1', 'post has correct id'); assert.strictEqual(post?.name, 'Krystan rules, you drool', 'post has correct name'); @@ -123,7 +125,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { }); test('queryRecord', function (assert) { - const result = queryRecord('post', { id: '1' }); + const result = queryRecord('post', { id: '1' }); assert.deepEqual( result, { @@ -144,7 +146,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { whatever: true, adapterOptions: {}, }; - const result = queryRecord('post', { id: '1' }, options); + const result = queryRecord('post', { id: '1' }, options); assert.deepEqual( result, { diff --git a/tests/main/tests/integration/legacy-compat/save-record-test.ts b/tests/main/tests/integration/legacy-compat/save-record-test.ts index 27d295207ce..b37f9cc94b0 100644 --- a/tests/main/tests/integration/legacy-compat/save-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -2,11 +2,14 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import type { CompatStore } from '@ember-data/legacy-compat'; import { saveRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; -import Store, { recordIdentifierFor } from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; class Post extends Model { + [Type] = 'post' as const; @attr declare name: string; } @@ -42,9 +45,9 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }) as Post; - const { content: savedPost } = await store.request(saveRecord(newPost)); + const store = this.owner.lookup('service:store') as CompatStore; + const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const { content: savedPost } = await store.request(saveRecord(newPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Krystan rules, you drool', 'post has correct name'); @@ -52,8 +55,8 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { }); test('saveRecord', function (assert) { - const store = this.owner.lookup('service:store') as Store; - const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }) as Post; + const store = this.owner.lookup('service:store') as CompatStore; + const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }); const identifier = recordIdentifierFor(newPost); const result = saveRecord(newPost); assert.deepEqual( @@ -76,8 +79,8 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { whatever: true, adapterOptions: {}, }; - const store = this.owner.lookup('service:store') as Store; - const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }) as Post; + const store = this.owner.lookup('service:store') as CompatStore; + const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); const identifier = recordIdentifierFor(newPost); const result = saveRecord(newPost, options); assert.deepEqual( @@ -111,8 +114,8 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const existingPost = store.push({ + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost = store.push({ data: { id: '1', type: 'post', @@ -120,9 +123,9 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); - const { content: savedPost } = await store.request(saveRecord(existingPost)); + const { content: savedPost } = await store.request(saveRecord(existingPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Krystan rules, you drool', 'post has correct name'); @@ -131,7 +134,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { }); test('saveRecord', function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as CompatStore; const existingPost: Post = store.push({ data: { id: '1', @@ -140,7 +143,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost); @@ -164,7 +167,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { whatever: true, adapterOptions: {}, }; - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as CompatStore; const existingPost: Post = store.push({ data: { id: '1', @@ -173,7 +176,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost, options); @@ -208,8 +211,8 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { } ); - const store = this.owner.lookup('service:store') as Store; - const existingPost = store.push({ + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost = store.push({ data: { id: '1', type: 'post', @@ -217,9 +220,9 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; - const { content: savedPost } = await store.request(saveRecord(existingPost)); + const { content: savedPost } = await store.request(saveRecord(existingPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Chris drools, Krystan rules', 'post has correct name'); @@ -228,7 +231,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { }); test('saveRecord', function (assert) { - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as CompatStore; const existingPost: Post = store.push({ data: { id: '1', @@ -237,7 +240,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost); @@ -261,7 +264,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { whatever: true, adapterOptions: {}, }; - const store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as CompatStore; const existingPost: Post = store.push({ data: { id: '1', @@ -270,7 +273,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost, options); diff --git a/tests/main/tests/integration/model-errors-test.gts b/tests/main/tests/integration/model-errors-test.gts new file mode 100644 index 00000000000..7dd4cb720a7 --- /dev/null +++ b/tests/main/tests/integration/model-errors-test.gts @@ -0,0 +1,68 @@ +import 'qunit-dom'; // tell TS consider *.dom extension for assert + +import { get } from '@ember/object'; +import type Owner from '@ember/owner'; +import { render, settled } from '@ember/test-helpers'; +import Component from '@glimmer/component'; + +import { module, test } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; +import type Store from '@ember-data/store'; + +class Tag extends Model { + @attr('string', {}) + name; +} + +class ErrorList extends Component<{ model: Model; field: string }> { + get errors() { + const { model, field } = this.args; + return model.errors.errorsFor(field).map((error) => error.message); + } + + +} + +interface CurrentTestContext { + tag: Tag; + owner: Owner; +} + +module('integration/model.errors', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function (this: CurrentTestContext) { + const { owner } = this; + + owner.register('model:tag', Tag); + }); + + test('Model errors are autotracked', async function (this: CurrentTestContext, assert) { + const tag = (this.tag = (this.owner.lookup('service:store') as Store).createRecord('tag', {}) as Tag); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errors: any = get(this.tag, 'errors'); + + await render(); + + assert.dom('.error-list__error').doesNotExist(); + + errors.add('name', 'the-error'); + await settled(); + + assert.dom('.error-list__error').hasText('the-error'); + + errors.remove('name'); + await settled(); + + assert.dom('.error-list__error').doesNotExist(); + }); +}); diff --git a/tests/main/tests/integration/model-errors-test.ts b/tests/main/tests/integration/model-errors-test.ts deleted file mode 100644 index 89fcbba628e..00000000000 --- a/tests/main/tests/integration/model-errors-test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import 'qunit-dom'; // tell TS consider *.dom extension for assert - -// @ts-ignore -import { setComponentTemplate } from '@ember/component'; -import { get } from '@ember/object'; -import { render, settled } from '@ember/test-helpers'; -import Component from '@glimmer/component'; - -import { module, test } from 'qunit'; - -import { hbs } from 'ember-cli-htmlbars'; -import { setupRenderingTest } from 'ember-qunit'; - -import Model, { attr } from '@ember-data/model'; - -class Tag extends Model { - @attr('string', {}) - name; -} - -class ErrorList extends Component<{ model: Model; field: string }> { - get errors() { - const { model, field } = this.args; - return model.errors.errorsFor(field).map((error) => error.message); - } -} - -const template = hbs` -
            - {{#each this.errors as |error|}} -
          • {{error}}
          • - {{/each}} -
          -`; - -interface CurrentTestContext { - tag: Tag; - owner: any; -} - -module('integration/model.errors', function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function (this: CurrentTestContext) { - let { owner } = this; - - owner.register('model:tag', Tag); - owner.register('component:error-list', setComponentTemplate(template, ErrorList)); - }); - - test('Model errors are autotracked', async function (this: CurrentTestContext, assert) { - this.tag = this.owner.lookup('service:store').createRecord('tag'); - const errors: any = get(this.tag, 'errors'); - - await render(hbs``); - - assert.dom('.error-list__error').doesNotExist(); - - errors.add('name', 'the-error'); - await settled(); - - assert.dom('.error-list__error').hasText('the-error'); - - errors.remove('name'); - await settled(); - - assert.dom('.error-list__error').doesNotExist(); - }); -}); diff --git a/tests/main/tests/integration/peek-all-test.js b/tests/main/tests/integration/peek-all-test.js index b991f2f067c..2737dbac42c 100644 --- a/tests/main/tests/integration/peek-all-test.js +++ b/tests/main/tests/integration/peek-all-test.js @@ -18,7 +18,7 @@ module('integration/peek-all - DS.Store#peekAll()', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); store = owner.lookup('service:store'); @@ -44,7 +44,7 @@ module('integration/peek-all - DS.Store#peekAll()', function (hooks) { ], }); - let all = store.peekAll('person'); + const all = store.peekAll('person'); assert.strictEqual(get(all, 'length'), 2); store.push({ diff --git a/tests/main/tests/integration/polymorphic-belongs-to-test.js b/tests/main/tests/integration/polymorphic-belongs-to-test.js index 30321d89a1b..645490f4462 100644 --- a/tests/main/tests/integration/polymorphic-belongs-to-test.js +++ b/tests/main/tests/integration/polymorphic-belongs-to-test.js @@ -9,7 +9,7 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; class Book extends Model { @attr() title; @@ -38,7 +38,7 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h }); test('using store.push with a null value for a payload in relationships sets the Models relationship to null - sync relationship', function (assert) { - let payload = { + const payload = { data: { type: 'book', id: '1', @@ -62,10 +62,10 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h }; store.push(payload); - let book = store.peekRecord('book', 1); + const book = store.peekRecord('book', 1); assert.strictEqual(book.author.id, '1'); - let payloadThatResetsBelongToRelationship = { + const payloadThatResetsBelongToRelationship = { data: { type: 'book', id: '1', @@ -83,7 +83,7 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h }); test('using store.push with a null value for a payload in relationships sets the Models relationship to null - async relationship', function (assert) { - let payload = { + const payload = { data: { type: 'async-book', id: '1', @@ -107,9 +107,9 @@ module('integration/polymorphic-belongs-to - Polymorphic BelongsTo', function (h }; store.push(payload); - let book = store.peekRecord('async-book', 1); + const book = store.peekRecord('async-book', 1); - let payloadThatResetsBelongToRelationship = { + const payloadThatResetsBelongToRelationship = { data: { type: 'async-book', id: '1', diff --git a/tests/main/tests/integration/record-array-manager-test.js b/tests/main/tests/integration/record-array-manager-test.js index 1d13fb62840..f2438725e71 100644 --- a/tests/main/tests/integration/record-array-manager-test.js +++ b/tests/main/tests/integration/record-array-manager-test.js @@ -33,7 +33,7 @@ class Car extends Model { module('integration/record_array_manager', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('adapter:application', RESTAdapter); owner.register('model:car', Car); owner.register('model:person', Person); @@ -59,7 +59,7 @@ module('integration/record_array_manager', function (hooks) { }, }); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -74,10 +74,10 @@ module('integration/record_array_manager', function (hooks) { }, }); - let all = store.peekAll('person'); - let query = {}; - let adapterPopulated = manager.createArray({ type: 'person', query }); - let identifier = recordIdentifierFor(person); + const all = store.peekAll('person'); + const query = {}; + const adapterPopulated = manager.createArray({ type: 'person', query }); + const identifier = recordIdentifierFor(person); assert.false(all.isDestroyed, 'initial: LiveArray is not destroyed'); assert.false(adapterPopulated.isDestroyed, 'initial: Collection is not destroyed'); @@ -101,7 +101,7 @@ module('integration/record_array_manager', function (hooks) { assert.true(adapterPopulated.isDestroyed, 'Collection is destroyed'); }); - test('#GH-4041 store#query AdapterPopulatedRecordArrays are removed from their managers instead of retained when #destroy is called', async function (assert) { + test('#GH-4041 store#query CollectionRecordArrays are removed from their managers instead of retained when #destroy is called', async function (assert) { store.push({ data: { type: 'car', @@ -115,7 +115,7 @@ module('integration/record_array_manager', function (hooks) { const query = {}; - let adapterPopulated = manager.createArray({ type: 'car', query }); + const adapterPopulated = manager.createArray({ type: 'car', query }); adapterPopulated.destroy(); await settled(); @@ -124,7 +124,7 @@ module('integration/record_array_manager', function (hooks) { }); test('liveArrayFor (base)', function (assert) { - let recordArray = manager.liveArrayFor('foo'); + const recordArray = manager.liveArrayFor('foo'); assert.strictEqual(recordArray.modelName, 'foo'); assert.true(recordArray.isLoaded); diff --git a/tests/main/tests/integration/record-array-test.js b/tests/main/tests/integration/record-array-test.js index 3ca3a3b1570..398fe178d30 100644 --- a/tests/main/tests/integration/record-array-test.js +++ b/tests/main/tests/integration/record-array-test.js @@ -2,13 +2,13 @@ import { get } from '@ember/object'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; class Person extends Model { @attr() @@ -27,18 +27,18 @@ class Tool extends Model { person; } -module('integration/record-array - RecordArray', function (hooks) { +module('integration/live-array', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); }); test('acts as a live query', async function (assert) { const store = this.owner.lookup('service:store'); - let recordArray = store.peekAll('person'); + const recordArray = store.peekAll('person'); store.push({ data: { @@ -69,46 +69,54 @@ module('integration/record-array - RecordArray', function (hooks) { assert.strictEqual(recordArray.at(-1).name, 'brohuda'); }); - test('acts as a live query (normalized names)', async function (assert) { - const store = this.owner.lookup('service:store'); + deprecatedTest( + 'acts as a live query (normalized names)', + { + count: 9, + until: '6.0', + id: 'ember-data:deprecate-non-strict-types', + }, + async function (assert) { + const store = this.owner.lookup('service:store'); - let recordArray = store.peekAll('Person'); - let otherRecordArray = store.peekAll('person'); + const recordArray = store.peekAll('Person'); + const otherRecordArray = store.peekAll('person'); - assert.strictEqual(recordArray, otherRecordArray, 'Person and person are the same record-array'); + assert.strictEqual(recordArray, otherRecordArray, 'Person and person are the same record-array'); - store.push({ - data: { - type: 'Person', - id: '1', - attributes: { - name: 'John Churchill', + store.push({ + data: { + type: 'Person', + id: '1', + attributes: { + name: 'John Churchill', + }, }, - }, - }); + }); - assert.deepEqual( - recordArray.map((v) => v.name), - ['John Churchill'] - ); + assert.deepEqual( + recordArray.map((v) => v.name), + ['John Churchill'] + ); - store.push({ - data: { - type: 'Person', - id: '2', - attributes: { - name: 'Winston Churchill', + store.push({ + data: { + type: 'Person', + id: '2', + attributes: { + name: 'Winston Churchill', + }, }, - }, - }); + }); - assert.deepEqual( - recordArray.map((v) => v.name), - ['John Churchill', 'Winston Churchill'] - ); - }); + assert.deepEqual( + recordArray.map((v) => v.name), + ['John Churchill', 'Winston Churchill'] + ); + } + ); - test('a loaded record is removed from a record array when it is deleted', async function (assert) { + test('a loaded record is removed from a live record array when it is deleted', async function (assert) { assert.expect(5); this.owner.register('model:tag', Tag); @@ -117,7 +125,7 @@ module('integration/record-array - RecordArray', function (hooks) { 'adapter:application', Adapter.extend({ deleteRecord() { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }, shouldBackgroundReloadRecord() { return false; @@ -157,9 +165,9 @@ module('integration/record-array - RecordArray', function (hooks) { ], }); - let scumbag = await store.findRecord('person', '1'); - let tag = await store.findRecord('tag', '1'); - let recordArray = tag.people; + const scumbag = await store.findRecord('person', '1'); + const tag = await store.findRecord('tag', '1'); + const recordArray = tag.people; recordArray.push(scumbag); @@ -296,7 +304,7 @@ module('integration/record-array - RecordArray', function (hooks) { 'adapter:application', Adapter.extend({ deleteRecord() { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }, }) ); @@ -323,8 +331,8 @@ module('integration/record-array - RecordArray', function (hooks) { ], }); - let scumbag = store.peekRecord('person', 1); - let tag = store.peekRecord('tag', 1); + const scumbag = store.peekRecord('person', 1); + const tag = store.peekRecord('tag', 1); scumbag.deleteRecord(); @@ -337,7 +345,7 @@ module('integration/record-array - RecordArray', function (hooks) { 'adapter:application', Adapter.extend({ deleteRecord() { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }, }) ); @@ -375,9 +383,9 @@ module('integration/record-array - RecordArray', function (hooks) { ], }); - let scumbag = store.peekRecord('person', 1); - let tag = store.peekRecord('tag', 1); - let tool = store.peekRecord('tool', 1); + const scumbag = store.peekRecord('person', 1); + const tag = store.peekRecord('tag', 1); + const tool = store.peekRecord('tool', 1); assert.strictEqual(tag.people.length, 1, 'record is in the record array'); assert.strictEqual(tool.person, scumbag, 'the tool belongs to the record'); @@ -392,8 +400,8 @@ module('integration/record-array - RecordArray', function (hooks) { test('a newly created record is removed from a record array when it is deleted', async function (assert) { const store = this.owner.lookup('service:store'); - let recordArray = store.peekAll('person'); - let scumbag = store.createRecord('person', { + const recordArray = store.peekAll('person'); + const scumbag = store.createRecord('person', { name: 'Scumbag Dale', }); @@ -443,7 +451,7 @@ module('integration/record-array - RecordArray', function (hooks) { ], }); - let recordArray = store.peekAll('person'); + const recordArray = store.peekAll('person'); assert.strictEqual(recordArray.at(20), undefined, 'objects outside of the range just return undefined'); }); @@ -478,22 +486,22 @@ module('integration/record-array - RecordArray', function (hooks) { ], }); - let recordArray = store.peekAll('person'); + const recordArray = store.peekAll('person'); assert.strictEqual(recordArray.at(2).id, '3', 'should retrieve correct record at index 2'); assert.strictEqual(recordArray.at(1).id, '2', 'should retrieve correct record at index 1'); assert.strictEqual(recordArray.at(0).id, '1', 'should retrieve correct record at index 0'); }); - test("an AdapterPopulatedRecordArray knows if it's loaded or not", async function (assert) { + test("a CollectionRecordArray knows if it's loaded or not", async function (assert) { const store = this.owner.lookup('service:store'); assert.expect(2); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); adapter.query = function (store, type, query, recordArray) { assert.false(recordArray.isLoaded, 'not loaded yet'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }, { id: '2', type: 'person', attributes: { name: 'Scumbag Katz' } }, @@ -502,7 +510,7 @@ module('integration/record-array - RecordArray', function (hooks) { }); }; - let people = await store.query('person', { page: 1 }); + const people = await store.query('person', { page: 1 }); assert.true(people.isLoaded, 'The array is now loaded'); }); 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 8693513c3a4..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 @@ -1,7 +1,6 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -17,7 +16,7 @@ const Person = Model.extend({ }, }); -module('integration/record-arrays/adapter_populated_record_array - AdapterPopulatedRecordArray', function (hooks) { +module('integration/record-arrays/collection', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { @@ -35,10 +34,10 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula this.owner.register('adapter:application', ApplicationAdapter); - let store = this.owner.lookup('service:store'); - let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); - let payload = { + const payload = { data: [ { type: 'person', @@ -64,7 +63,7 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula ], }; - let results = store._push(payload); + const results = store._push(payload); store.recordArrayManager.populateManagedArray(recordArray, results, payload); @@ -76,10 +75,10 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula }); test('stores the metadata off the payload', async function (assert) { - let store = this.owner.lookup('service:store'); - let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); - let payload = { + const payload = { data: [ { type: 'person', @@ -108,17 +107,17 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula }, }; - let results = store._push(payload); + const results = store._push(payload); store.recordArrayManager.populateManagedArray(recordArray, results, payload); assert.strictEqual(recordArray.meta.foo, 'bar', 'expected meta.foo to be bar from payload'); }); test('stores the links off the payload', async function (assert) { - let store = this.owner.lookup('service:store'); - let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); - let payload = { + const payload = { data: [ { type: 'person', @@ -147,15 +146,30 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula }, }; - let results = store._push(payload); + const results = store._push(payload); store.recordArrayManager.populateManagedArray(recordArray, results, payload); assert.strictEqual(recordArray.links.first, '/foo?page=1', 'expected links.first to be "/foo?page=1" from payload'); }); + test('recordArray.splice() 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.splice(0, 1); + }, + 'Mutating this array of records via splice is not allowed.', + 'throws error' + ); + }); + test('recordArray.replace() throws error', async function (assert) { - let store = this.owner.lookup('service:store'); - let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); await settled(); @@ -163,15 +177,15 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula () => { recordArray.replace(); }, - 'Assertion Failed: Mutating this array of records via splice is not allowed.', + '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) { - let store = this.owner.lookup('service:store'); - let recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); await settled(); @@ -179,23 +193,23 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula () => { recordArray.splice(0, 1); }, - 'Assertion Failed: Mutating this array of records via splice is not allowed.', + 'Mutating this array of records via splice is not allowed.', 'throws error' ); }); test('pass record array to adapter.query regardless of its arity', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let payload = { + const payload = { data: [ { id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }, { id: '2', type: 'person', attributes: { name: 'Scumbag Katz' } }, ], }; - let actualQuery = {}; + const actualQuery = {}; // arity 3 adapter.query = function (store, type, query) { @@ -216,8 +230,8 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula }); test('loadRecord re-syncs identifiers recordArrays', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let payload = { data: [ @@ -258,10 +272,9 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula test('when an adapter populated record gets updated the array contents are also updated', async function (assert) { assert.expect(8); - let queryArr, findArray; - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let array = [{ id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }]; + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const array = [{ id: '1', type: 'person', attributes: { name: 'Scumbag Dale' } }]; // resemble server side filtering adapter.query = function (store, type, query, recordArray) { @@ -274,8 +287,8 @@ module('integration/record-arrays/adapter_populated_record_array - AdapterPopula return { data: array.slice(0) }; }; - queryArr = await store.query('person', { slice: 1 }); - findArray = await store.findAll('person'); + const queryArr = await store.query('person', { slice: 1 }); + const findArray = await store.findAll('person'); assert.strictEqual(queryArr.length, 0, 'No records for this query'); assert.false(queryArr.isUpdating, 'Record array isUpdating state updated'); diff --git a/tests/main/tests/integration/record-arrays/peeked-records-test.js b/tests/main/tests/integration/record-arrays/peeked-records-test.js index 266187b8a5a..a9bf46b6ff1 100644 --- a/tests/main/tests/integration/record-arrays/peeked-records-test.js +++ b/tests/main/tests/integration/record-arrays/peeked-records-test.js @@ -27,8 +27,8 @@ module('integration/peeked-records', function (hooks) { }); test('repeated calls to peekAll in separate run-loops works as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial access'); @@ -69,8 +69,8 @@ module('integration/peeked-records', function (hooks) { }); test('peekAll in the same run-loop as push works as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); @@ -112,15 +112,16 @@ module('integration/peeked-records', function (hooks) { }); test('newly created records notify the array as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); - let aNewlyCreatedRecord = store.createRecord('person', { + const aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); + await settled(); assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); @@ -131,18 +132,21 @@ module('integration/peeked-records', function (hooks) { }); test('immediately peeking newly created records works as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.strictEqual(peekedRecordArray.length, 0); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state initial'); - let aNewlyCreatedRecord = store.createRecord('person', { + const aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); let records = store.peekAll('person'); - assert.strictEqual(records.length, 1); + assert.strictEqual(records.length, 1, 'we see the new record'); + + // we should not have notified the array yet because ember schedules this async + await settled(); assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); aNewlyCreatedRecord.unloadRecord(); @@ -155,14 +159,17 @@ module('integration/peeked-records', function (hooks) { }); test('unloading newly created records notify the array as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); - let aNewlyCreatedRecord = store.createRecord('person', { + const aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); + // we should not have notified the array yet because ember schedules this async + await settled(); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); aNewlyCreatedRecord.unloadRecord(); @@ -172,15 +179,17 @@ module('integration/peeked-records', function (hooks) { }); test('immediately peeking after unloading newly created records works as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = store.peekAll('person'); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); - let aNewlyCreatedRecord = store.createRecord('person', { + const aNewlyCreatedRecord = store.createRecord('person', { name: 'James', }); + await settled(); + assert.watchedPropertyCounts(watcher, { length: 2, '[]': 2 }, 'RecordArray state when a new record is created'); aNewlyCreatedRecord.unloadRecord(); @@ -191,9 +200,9 @@ module('integration/peeked-records', function (hooks) { }); test('unloadAll followed by peekAll in the same run-loop works as expected', async function (assert) { - let peekedRecordArray = store.peekAll('person'); + const peekedRecordArray = store.peekAll('person'); assert.strictEqual(peekedRecordArray.length, 0, 'length is 0'); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); @@ -280,8 +289,8 @@ module('integration/peeked-records', function (hooks) { return result; } - let peekedRecordArray = await peek(); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = await peek(); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); @@ -333,8 +342,8 @@ module('integration/peeked-records', function (hooks) { return result; } - let peekedRecordArray = await peek(); - let watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); + const peekedRecordArray = await peek(); + const watcher = watchProperties.call(this, peekedRecordArray, ['length', '[]']); await startWatching.call(this); assert.watchedPropertyCounts(watcher, { length: 1, '[]': 1 }, 'RecordArray state init'); diff --git a/tests/main/tests/integration/record-data/record-data-errors-test.ts b/tests/main/tests/integration/record-data/record-data-errors-test.ts deleted file mode 100644 index 8a33b74e955..00000000000 --- a/tests/main/tests/integration/record-data/record-data-errors-test.ts +++ /dev/null @@ -1,699 +0,0 @@ -import EmberObject from '@ember/object'; - -import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; - -import Store from 'ember-data/store'; -import { setupTest } from 'ember-qunit'; - -import { InvalidError } from '@ember-data/adapter/error'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import Model, { attr } from '@ember-data/model'; -import type { StructuredDataDocument } from '@ember-data/request/-private/types'; -import JSONAPISerializer from '@ember-data/serializer/json-api'; -import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceBlob } from '@ember-data/types/cache/aliases'; -import type { Change } from '@ember-data/types/cache/change'; -import type { - CollectionResourceDataDocument, - ResourceDocument, - ResourceErrorDocument, - ResourceMetaDocument, - SingleResourceDataDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { DSModel } from '@ember-data/types/q/ds-model'; -import type { - CollectionResourceDocument, - CollectionResourceRelationship, - JsonApiDocument, - SingleResourceDocument, - SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { - NewRecordIdentifier, - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import type { JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; - -if (!DEPRECATE_V1_RECORD_DATA) { - class Person extends Model { - @attr declare firstName: string; - @attr declare lastName: string; - } - - class TestRecordData implements Cache { - wrapper: CacheStoreWrapper; - _data: Map = new Map(); - constructor(wrapper: CacheStoreWrapper) { - this.wrapper = wrapper; - } - patch(op: MergeOperation): void { - throw new Error('Method not implemented.'); - } - put(doc: StructuredDocument): SingleResourceDataDocument; - put(doc: StructuredDocument): CollectionResourceDataDocument; - put( - doc: StructuredDocument - ): ResourceMetaDocument | ResourceErrorDocument; - put(doc: StructuredDocument): ResourceDocument { - if ('content' in doc && !('error' in doc)) { - const identifier = this.wrapper.identifierCache.getOrCreateRecordIdentifier( - doc.content.data as RecordIdentifier - ); - this.upsert(identifier, doc.content.data as JsonApiResource, this.wrapper.hasRecord(identifier)); - return { data: identifier } as SingleResourceDataDocument; - } else if ('error' in doc) { - throw typeof doc.error === 'string' ? new Error(doc.error) : doc.error; - } - throw new Error('Not Implemented'); - } - - peek(identifier: StableRecordIdentifier): ResourceBlob | null; - peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { - throw new Error(`Not Implemented`); - } - peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { - throw new Error(`Not Implemented`); - } - fork(): Promise { - throw new Error(`Not Implemented`); - } - merge(cache: Cache): Promise { - throw new Error(`Not Implemented`); - } - diff(): Promise { - throw new Error(`Not Implemented`); - } - dump(): Promise> { - throw new Error(`Not Implemented`); - } - hydrate(stream: ReadableStream): Promise { - throw new Error('Not Implemented'); - } - - mutate(operation: LocalRelationshipOperation): void { - throw new Error('Method not implemented.'); - } - version: '2' = '2'; - - _errors?: JsonApiError[]; - _isNew: boolean = false; - - upsert( - identifier: StableRecordIdentifier, - data: JsonApiResource, - calculateChanges?: boolean | undefined - ): void | string[] { - if (!this._data.has(identifier)) { - this.wrapper.notifyChange(identifier, 'added'); - } - this._data.set(identifier, data); - this.wrapper.notifyChange(identifier, 'attributes'); - this.wrapper.notifyChange(identifier, 'relationships'); - } - clientDidCreate( - identifier: StableRecordIdentifier, - options?: Record | undefined - ): Record { - this._isNew = true; - return {}; - } - willCommit(identifier: StableRecordIdentifier): void {} - didCommit( - identifier: StableRecordIdentifier, - response: StructuredDataDocument - ): SingleResourceDataDocument { - return { data: identifier as StableExistingRecordIdentifier }; - } - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[] | undefined): void { - this._errors = errors; - } - unloadRecord(identifier: StableRecordIdentifier): void {} - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - return ''; - } - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - throw new Error('Method not implemented.'); - } - changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - return {}; - } - hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - return false; - } - rollbackAttrs(identifier: StableRecordIdentifier): string[] { - throw new Error('Method not implemented.'); - } - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string - ): SingleResourceRelationship | CollectionResourceRelationship { - throw new Error('Method not implemented.'); - } - addToHasMany( - identifier: StableRecordIdentifier, - propertyName: string, - value: StableRecordIdentifier[], - idx?: number | undefined - ): void { - throw new Error('Method not implemented.'); - } - removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { - throw new Error('Method not implemented.'); - } - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { - return this._errors || []; - } - isEmpty(identifier: StableRecordIdentifier): boolean { - return false; - } - isNew(identifier: StableRecordIdentifier): boolean { - return this._isNew; - } - isDeleted(identifier: StableRecordIdentifier): boolean { - return false; - } - isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return false; - } - } - - module('integration/record-data Custom RecordData (v2) Errors', function (hooks) { - setupTest(hooks); - - test('RecordData Invalid Errors', async function (assert) { - assert.expect(3); - - const { owner } = this; - - class LifecycleRecordData extends TestRecordData { - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[]) { - super.commitWasRejected(identifier, errors); - assert.strictEqual(errors?.[0]?.detail, 'is a generally unsavoury character', 'received the error'); - assert.strictEqual(errors?.[0]?.source?.pointer, '/data/attributes/name', 'pointer is correct'); - } - } - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(wrapper) as Cache; - } - } - class TestAdapter extends EmberObject { - updateRecord() { - return Promise.reject( - new InvalidError([ - { - title: 'Invalid Attribute', - detail: 'is a generally unsavoury character', - source: { - pointer: '/data/attributes/name', - }, - }, - ]) - ); - } - - createRecord() { - return Promise.resolve(); - } - } - - owner.register('model:person', Person); - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter); - - const store = owner.lookup('service:store') as Store; - - const person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom', - }, - }, - }); - - try { - await (person as DSModel).save(); - assert.ok(false, 'we should error'); - } catch (error) { - assert.ok(true, 'we erred'); - } - }); - - test('RecordData Network Errors', async function (assert) { - assert.expect(2); - - const { owner } = this; - - class LifecycleRecordData extends TestRecordData { - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[]) { - super.commitWasRejected(identifier, errors); - assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); - } - } - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(wrapper) as Cache; - } - } - class TestAdapter extends EmberObject { - updateRecord() { - return Promise.reject(); - } - - createRecord() { - return Promise.resolve(); - } - } - - owner.register('model:person', Person); - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter); - - const store = owner.lookup('service:store') as Store; - - const person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom', - }, - }, - }); - - try { - await (person as DSModel).save(); - assert.ok(false, 'we should error'); - } catch (error) { - assert.ok(true, 'we erred'); - } - }); - - test('RecordData Invalid Errors Can Be Reflected On The Record', async function (assert) { - const { owner } = this; - let errorsToReturn: JsonApiError[] | undefined; - let storeWrapper; - - class LifecycleRecordData extends TestRecordData { - getErrors(): JsonApiError[] { - return errorsToReturn || []; - } - } - - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - storeWrapper = wrapper; - return new LifecycleRecordData(wrapper) as Cache; - } - } - - owner.register('model:person', Person); - owner.register('service:store', TestStore); - - const store = owner.lookup('service:store') as Store; - - const person: DSModel = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Tom', - lastName: 'Dale', - }, - }, - }) as DSModel; - - const identifier = recordIdentifierFor(person); - let nameError = person.errors.errorsFor('firstName').objectAt(0); - assert.strictEqual(nameError, undefined, 'no error shows up on firstName initially'); - assert.true(person.isValid, 'person is initially valid'); - - errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/firstName', - }, - }, - ]; - storeWrapper.notifyChange(identifier, 'errors'); - - nameError = person.errors.errorsFor('firstName').objectAt(0); - assert.strictEqual(nameError?.attribute, 'firstName', 'error shows up on name'); - assert.false(person.isValid, 'person is not valid'); - - errorsToReturn = []; - storeWrapper.notifyChange(identifier, 'errors'); - - assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on name'); - assert.true(person.isValid, 'person is valid'); - - errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/lastName', - }, - }, - ]; - storeWrapper.notifyChange(identifier, 'errors'); - - assert.false(person.isValid, 'person is not valid'); - assert.strictEqual(person.errors.errorsFor('firstName').length, 0, 'no errors on firstName'); - let lastNameError = person.errors.errorsFor('lastName').objectAt(0); - assert.strictEqual(lastNameError?.attribute, 'lastName', 'error shows up on lastName'); - }); - }); -} else { - module('integration/record-data - Custom RecordData (v1) Errors', function (hooks) { - setupTest(hooks); - - let store; - - class Person extends Model { - // TODO fix the typing for naked attrs - @attr('string', {}) - name; - - @attr('string', {}) - lastName; - } - - class TestRecordIdentifier implements NewRecordIdentifier { - constructor(public id: string | null, public lid: string, public type: string) {} - } - - class TestRecordData implements CacheV1 { - setIsDeleted(isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - version?: '1' | undefined = '1'; - isDeletionCommitted(): boolean { - return false; - } - id: string | null = '1'; - clientId: string | null = 'test-record-data-1'; - modelName = 'tst'; - - getResourceIdentifier() { - if (this.clientId !== null) { - return new TestRecordIdentifier(this.id, this.clientId, this.modelName); - } - } - - _errors: JsonApiError[] = []; - getErrors(recordIdentifier: RecordIdentifier): JsonApiError[] { - return this._errors; - } - commitWasRejected(identifier: StableRecordIdentifier, errors: JsonApiError[]): void { - this._errors = errors; - } - - // Use correct interface once imports have been fix - _storeWrapper: any; - - pushData(data: object, calculateChange: true): string[]; - pushData(data: object, calculateChange?: false): void; - pushData(data: object, calculateChange?: boolean): string[] | void {} - - clientDidCreate() {} - - willCommit() {} - - unloadRecord() {} - rollbackAttributes() { - return []; - } - changedAttributes(): any {} - - hasChangedAttributes(): boolean { - return false; - } - - setDirtyAttribute(key: string, value: any) {} - - getAttr(key: string): string { - return 'test'; - } - - getHasMany(key: string) { - return {}; - } - - isRecordInUse(): boolean { - return true; - } - - isNew() { - return false; - } - - isDeleted() { - return false; - } - - addToHasMany(key: string, recordDatas: Cache[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: Cache[]) {} - setDirtyHasMany(key: string, recordDatas: Cache[]) {} - - getBelongsTo(key: string) { - return {}; - } - - setDirtyBelongsTo(name: string, recordData: Cache | null) {} - - didCommit(data) {} - - _initRecordCreateOptions(options) { - return {}; - } - } - - class CustomStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new TestRecordData(); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new TestRecordData(wrapper) as Cache; - } - } - - hooks.beforeEach(function () { - let { owner } = this; - - owner.register('model:person', Person); - owner.unregister('service:store'); - owner.register('service:store', CustomStore); - owner.register('serializer:application', JSONAPISerializer); - }); - - test('Record Data invalid errors', async function (assert) { - assert.expect(3); - - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - - class LifecycleRecordData extends TestRecordData { - commitWasRejected(recordIdentifier, errors) { - super.commitWasRejected(recordIdentifier, errors); - assert.strictEqual(errors[0].detail, 'is a generally unsavoury character', 'received the error'); - assert.strictEqual(errors[0].source.pointer, '/data/attributes/name', 'pointer is correct'); - } - } - - class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new LifecycleRecordData(); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; - } - } - - let TestAdapter = EmberObject.extend({ - updateRecord() { - return Promise.reject( - new InvalidError([ - { - title: 'Invalid Attribute', - detail: 'is a generally unsavoury character', - source: { - pointer: '/data/attributes/name', - }, - }, - ]) - ); - }, - - createRecord() { - return Promise.resolve(); - }, - }); - - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); - - store = owner.lookup('service:store'); - - store.push({ - data: [personHash], - }); - let person = store.peekRecord('person', '1'); - await person.save().then( - () => {}, - (err) => {} - ); - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - }); - - test('Record Data adapter errors', async function (assert) { - assert.expect(2); - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - - class LifecycleRecordData extends TestRecordData { - commitWasRejected(recordIdentifier, errors) { - super.commitWasRejected(recordIdentifier, errors); - assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); - } - } - - class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; - } - } - - let TestAdapter = EmberObject.extend({ - updateRecord() { - return Promise.reject(); - }, - }); - - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); - - store = owner.lookup('service:store'); - - store.push({ - data: [personHash], - }); - let person = store.peekRecord('person', '1'); - await person.save().then( - () => {}, - (err) => {} - ); - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - }); - - test('Getting errors from Record Data shows up on the record', async function (assert) { - assert.expect(8); - let storeWrapper; - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - lastName: 'something', - }, - }; - let { owner } = this; - let errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/name', - }, - }, - ]; - - class LifecycleRecordData extends TestRecordData { - constructor(sw) { - super(); - storeWrapper = sw; - } - - getErrors(recordIdentifier: RecordIdentifier): JsonApiError[] { - return errorsToReturn; - } - } - - class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(wrapper); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; - } - } - - owner.register('service:store', TestStore); - store = owner.lookup('service:store'); - - store.push({ - data: [personHash], - }); - let person = store.peekRecord('person', '1'); - const identifier = recordIdentifierFor(person); - let nameError = person.errors.errorsFor('name').at(0); - assert.strictEqual(nameError.attribute, 'name', 'error shows up on name'); - assert.false(person.isValid, 'person is not valid'); - errorsToReturn = []; - storeWrapper.notifyChange(identifier, 'errors'); - assert.true(person.isValid, 'person is valid'); - assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); - errorsToReturn = [ - { - title: 'Invalid Attribute', - detail: '', - source: { - pointer: '/data/attributes/lastName', - }, - }, - ]; - storeWrapper.notifyChange(identifier, 'errors'); - assert.false(person.isValid, 'person is valid'); - assert.strictEqual(person.errors.errorsFor('name').length, 0, 'no errors on name'); - let lastNameError = person.errors.errorsFor('lastName').at(0); - assert.strictEqual(lastNameError.attribute, 'lastName', 'error shows up on lastName'); - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - }); - }); -} diff --git a/tests/main/tests/integration/record-data/record-data-state-test.ts b/tests/main/tests/integration/record-data/record-data-state-test.ts deleted file mode 100644 index 822422aecd2..00000000000 --- a/tests/main/tests/integration/record-data/record-data-state-test.ts +++ /dev/null @@ -1,515 +0,0 @@ -import EmberObject from '@ember/object'; -import { settled } from '@ember/test-helpers'; - -import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; - -import Store from 'ember-data/store'; -import { setupTest } from 'ember-qunit'; - -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import Model, { attr } from '@ember-data/model'; -import { StructuredDataDocument } from '@ember-data/request/-private/types'; -import JSONAPISerializer from '@ember-data/serializer/json-api'; -import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceBlob } from '@ember-data/types/cache/aliases'; -import type { Change } from '@ember-data/types/cache/change'; -import type { - CollectionResourceDataDocument, - ResourceDocument, - ResourceErrorDocument, - ResourceMetaDocument, - SingleResourceDataDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import type { - CollectionResourceDocument, - CollectionResourceRelationship, - JsonApiDocument, - SingleResourceDocument, - SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { - NewRecordIdentifier, - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import type { JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import { Dict } from '@ember-data/types/q/utils'; - -class Person extends Model { - // TODO fix the typing for naked attrs - @attr('string', {}) - name; - - @attr('string', {}) - lastName; -} - -class TestRecordIdentifier implements NewRecordIdentifier { - constructor(public id: string | null, public lid: string, public type: string) {} -} - -class V1TestRecordData implements CacheV1 { - _storeWrapper: CacheStoreWrapper; - _identifier: StableRecordIdentifier; - - constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { - this._storeWrapper = wrapper; - this._identifier = identifier; - } - - setIsDeleted(isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - version?: '1' | undefined; - isDeletionCommitted(): boolean { - throw new Error('Method not implemented.'); - } - id: string | null = '1'; - clientId: string | null = 'test-record-data-1'; - modelName = 'tst'; - _errors: JsonApiError[] = []; - getErrors(recordIdentifier: RecordIdentifier): JsonApiError[] { - return this._errors; - } - commitWasRejected(identifier: StableRecordIdentifier, errors: JsonApiError[]): void { - this._errors = errors; - } - - getResourceIdentifier() { - if (this.clientId !== null) { - return new TestRecordIdentifier(this.id, this.clientId, this.modelName); - } - } - - pushData(data: object, calculateChange: true): string[]; - pushData(data: object, calculateChange?: false): void; - pushData(data: object, calculateChange?: boolean): string[] | void { - this._storeWrapper.notifyChange(this._identifier, 'added'); - } - - clientDidCreate() { - this._storeWrapper.notifyChange(this._identifier, 'added'); - } - - willCommit() {} - - isRecordInUse() { - return false; - } - unloadRecord() {} - rollbackAttributes() { - return []; - } - changedAttributes(): any {} - - hasChangedAttributes(): boolean { - return false; - } - - setDirtyAttribute(key: string, value: any) {} - - getAttr(key: string): string { - return 'test'; - } - - getHasMany(key: string) { - return {}; - } - - isNew() { - return false; - } - - isDeleted() { - return false; - } - - addToHasMany(key: string, recordDatas: Cache[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: Cache[]) {} - setDirtyHasMany(key: string, recordDatas: Cache[]) {} - - getBelongsTo(key: string) { - return {}; - } - - setDirtyBelongsTo(name: string, recordData: Cache | null) {} - - didCommit(data) {} - - _initRecordCreateOptions(options) { - return {}; - } -} -class V2TestRecordData implements Cache { - _storeWrapper: CacheStoreWrapper; - _identifier: StableRecordIdentifier; - - constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { - this._storeWrapper = wrapper; - this._identifier = identifier; - } - - patch(op: MergeOperation): void { - throw new Error('Method not implemented.'); - } - _data: Map = new Map(); - put(doc: StructuredDocument): SingleResourceDataDocument; - put(doc: StructuredDocument): CollectionResourceDataDocument; - put( - doc: StructuredDocument - ): ResourceMetaDocument | ResourceErrorDocument; - put(doc: StructuredDocument): ResourceDocument { - if ('content' in doc && !('error' in doc)) { - if (Array.isArray(doc.content.data)) { - const data = doc.content.data.map((data) => { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier(data); - this.upsert(identifier, data, this._storeWrapper.hasRecord(identifier)); - return identifier; - }); - return { data }; - } else { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( - doc.content.data as RecordIdentifier - ); - this.upsert(identifier, doc.content.data as JsonApiResource, this._storeWrapper.hasRecord(identifier)); - return { data: identifier } as SingleResourceDataDocument; - } - } else if ('error' in doc) { - throw typeof doc.error === 'string' ? new Error(doc.error) : doc.error; - } - throw new Error('Not Implemented'); - } - - peek(identifier: StableRecordIdentifier): ResourceBlob | null; - peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { - throw new Error(`Not Implemented`); - } - peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { - throw new Error(`Not Implemented`); - } - fork(): Promise { - throw new Error(`Not Implemented`); - } - merge(cache: Cache): Promise { - throw new Error(`Not Implemented`); - } - diff(): Promise { - throw new Error(`Not Implemented`); - } - dump(): Promise> { - throw new Error(`Not Implemented`); - } - hydrate(stream: ReadableStream): Promise { - throw new Error('Not Implemented'); - } - - upsert( - identifier: StableRecordIdentifier, - data: JsonApiResource, - calculateChanges?: boolean | undefined - ): void | string[] { - if (!this._data.has(identifier)) { - this._storeWrapper.notifyChange(identifier, 'added'); - } - this._data.set(identifier, data); - this._storeWrapper.notifyChange(identifier, 'attributes'); - this._storeWrapper.notifyChange(identifier, 'relationships'); - } - mutate(operation: LocalRelationshipOperation): void { - throw new Error('Method not implemented.'); - } - version: '2' = '2'; - - _errors?: JsonApiError[]; - _isNew: boolean = false; - - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { - this._isNew = true; - this._storeWrapper.notifyChange(identifier, 'added'); - return {}; - } - willCommit(identifier: StableRecordIdentifier): void {} - didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { - return { data: identifier as StableExistingRecordIdentifier }; - } - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[] | undefined): void { - this._errors = errors; - } - unloadRecord(identifier: StableRecordIdentifier): void {} - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - return ''; - } - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - throw new Error('Method not implemented.'); - } - changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - return {}; - } - hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - return false; - } - rollbackAttrs(identifier: StableRecordIdentifier): string[] { - throw new Error('Method not implemented.'); - } - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string - ): SingleResourceRelationship | CollectionResourceRelationship { - throw new Error('Method not implemented.'); - } - addToHasMany( - identifier: StableRecordIdentifier, - propertyName: string, - value: StableRecordIdentifier[], - idx?: number | undefined - ): void { - throw new Error('Method not implemented.'); - } - removeFromHasMany(identifier: StableRecordIdentifier, propertyName: string, value: StableRecordIdentifier[]): void { - throw new Error('Method not implemented.'); - } - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { - return this._errors || []; - } - isEmpty(identifier: StableRecordIdentifier): boolean { - return false; - } - isNew(identifier: StableRecordIdentifier): boolean { - return this._isNew; - } - isDeleted(identifier: StableRecordIdentifier): boolean { - return false; - } - isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return false; - } -} -const TestRecordData = DEPRECATE_V1_RECORD_DATA ? V1TestRecordData : V2TestRecordData; - -class CustomStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new TestRecordData(wrapper, identifier); - } -} - -module('integration/record-data - Record Data State', function (hooks) { - setupTest(hooks); - - let store; - - hooks.beforeEach(function () { - let { owner } = this; - - owner.register('model:person', Person); - owner.unregister('service:store'); - owner.register('service:store', CustomStore); - owner.register('serializer:application', JSONAPISerializer); - }); - - test('Record Data state saving', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - - let isDeleted, isNew, isDeletionCommitted; - let calledDelete = false; - let calledUpdate = false; - let calledCreate = false; - - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - - class LifecycleRecordData extends TestRecordData { - isNew(): boolean { - return isNew; - } - - isDeleted(): boolean { - return isDeleted; - } - - isDeletionCommitted(): boolean { - return isDeletionCommitted; - } - - setIsDeleted(): void { - isDeleted = true; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(wrapper, identifier); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; - } - } - - let TestAdapter = EmberObject.extend({ - deleteRecord() { - calledDelete = true; - return Promise.resolve(); - }, - - updateRecord() { - calledUpdate = true; - return Promise.resolve(); - }, - - createRecord() { - calledCreate = true; - return Promise.resolve(); - }, - }); - - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); - - store = owner.lookup('service:store'); - - store.push({ - data: [personHash], - }); - - let person = store.peekRecord('person', '1'); - isNew = true; - await person.save(); - assert.true(calledCreate, 'called create if record isNew'); - - isNew = false; - isDeleted = true; - await person.save(); - assert.true(calledDelete, 'called delete if record isDeleted'); - - isNew = false; - isDeleted = false; - - await person.save(); - assert.true(calledUpdate, 'called update if record isnt deleted or new'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - }); - - test('Record Data state record flags', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 14 : 13); - let isDeleted, isNew, isDeletionCommitted; - let calledSetIsDeleted = false; - let storeWrapper; - - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - - class LifecycleRecordData extends TestRecordData { - constructor(sw: CacheStoreWrapper, identifier: StableRecordIdentifier) { - super(sw, identifier); - storeWrapper = sw; - } - - isEmpty(): boolean { - return !isNew && isDeletionCommitted; - } - - isNew(): boolean { - return isNew; - } - - isDeleted(): boolean { - return isDeleted; - } - - isDeletionCommitted(): boolean { - return isDeletionCommitted; - } - - setIsDeleted(value: boolean): void { - isDeleted = true; - calledSetIsDeleted = true; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new LifecycleRecordData(wrapper, identifier); - } - createCache(wrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; - } - } - - owner.register('service:store', TestStore); - - store = owner.lookup('service:store'); - - store.push({ - data: [personHash], - }); - - let person = store.peekRecord('person', '1'); - let personIdentifier = recordIdentifierFor(person); - let people = store.peekAll('person'); - assert.strictEqual(people.length, 1, 'live array starting length is 1'); - - isNew = true; - 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'); - await settled(); - assert.false(person.isNew, 'person is not new'); - assert.true(person.isDeleted, 'person is deleted'); - assert.strictEqual(people.length, 1, 'live array starting length is 1'); - - isNew = false; - isDeleted = false; - storeWrapper.notifyChange(personIdentifier, 'state'); - await settled(); - assert.false(person.isNew, 'person is not new'); - assert.false(person.isDeleted, 'person is not deleted'); - assert.strictEqual(people.length, 1, 'live array starting length is 1'); - person.deleteRecord(); - await settled(); - assert.strictEqual(people.length, 1, 'live array starting length is 1 after deleteRecord'); - assert.false(person.isDeleted, 'calling deleteRecord does not automatically set isDeleted flag to true'); - assert.true(calledSetIsDeleted, 'called setIsDeleted'); - - isDeletionCommitted = true; - storeWrapper.notifyChange(personIdentifier, 'state'); - await settled(); - assert.strictEqual(people.length, 0, 'commiting a deletion updates the live array'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - }); -}); diff --git a/tests/main/tests/integration/record-data/record-data-test.ts b/tests/main/tests/integration/record-data/record-data-test.ts deleted file mode 100644 index 17ced0f8604..00000000000 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ /dev/null @@ -1,599 +0,0 @@ -import EmberObject from '@ember/object'; -import { settled } from '@ember/test-helpers'; - -import { module, test } from 'qunit'; - -import Store from 'ember-data/store'; -import { setupTest } from 'ember-qunit'; - -import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { StructuredDataDocument } from '@ember-data/request/-private/types'; -import JSONAPISerializer from '@ember-data/serializer/json-api'; -import { ResourceBlob } from '@ember-data/types/cache/aliases'; -import { Change } from '@ember-data/types/cache/change'; -import { - CollectionResourceDataDocument, - ResourceDocument, - ResourceErrorDocument, - ResourceMetaDocument, - SingleResourceDataDocument, - StructuredDocument, -} from '@ember-data/types/cache/document'; -import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; -import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import { DSModel } from '@ember-data/types/q/ds-model'; -import type { - CollectionResourceDocument, - CollectionResourceRelationship, - JsonApiDocument, - SingleResourceDocument, - SingleResourceRelationship, -} from '@ember-data/types/q/ember-data-json-api'; -import type { - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; -import type { JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; -import type { Dict } from '@ember-data/types/q/utils'; - -class Person extends Model { - // TODO fix the typing for naked attrs - @attr('string', {}) - name; -} - -class House extends Model { - // TODO fix the typing for naked attrs - @attr('string', {}) - name; - - @belongsTo('person', { async: false, inverse: null }) - landlord; - - @hasMany('person', { async: false, inverse: null }) - tenants; -} - -// TODO: this should work -// class TestRecordData implements RecordDatav1 -class V1TestRecordData { - _storeWrapper: CacheStoreWrapper; - _identifier: StableRecordIdentifier; - - constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { - this._storeWrapper = wrapper; - this._identifier = identifier; - } - - pushData(data: object, calculateChange: true): string[]; - pushData(data: object, calculateChange?: false): void; - pushData(data: object, calculateChange?: boolean): string[] | void { - this._storeWrapper.notifyChange(this._identifier, 'added'); - } - - clientDidCreate() {} - - willCommit() {} - - _errors: JsonApiError[] = []; - getErrors(recordIdentifier: StableRecordIdentifier): JsonApiError[] { - return this._errors; - } - commitWasRejected(identifier: StableRecordIdentifier, errors: JsonApiError[]): void { - this._errors = errors; - } - - unloadRecord() {} - rollbackAttributes() {} - changedAttributes(): any {} - - hasChangedAttributes(): boolean { - return false; - } - - setDirtyAttribute(key: string, value: any) {} - - getAttr(key: string): string { - return 'test'; - } - - getHasMany(key: string) { - return {}; - } - - addToHasMany(key: string, recordDatas: this[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: this[]) {} - setDirtyHasMany(key: string, recordDatas: this[]) {} - - getBelongsTo(key: string) {} - - setDirtyBelongsTo(name: string, recordData: this | null) {} - - didCommit(data) {} - - isDeletionCommitted() { - return false; - } - - _initRecordCreateOptions(options) {} - isNew() { - return false; - } - isDeleted() { - return false; - } -} - -class V2TestRecordData implements Cache { - version: '2' = '2'; - - _errors?: JsonApiError[]; - _isNew: boolean = false; - _storeWrapper: CacheStoreWrapper; - _identifier: StableRecordIdentifier; - - constructor(wrapper: CacheStoreWrapper, identifier: StableRecordIdentifier) { - this._storeWrapper = wrapper; - this._identifier = identifier; - } - patch(op: MergeOperation): void { - throw new Error('Method not implemented.'); - } - _data: Map = new Map(); - put(doc: StructuredDocument): SingleResourceDataDocument; - put(doc: StructuredDocument): CollectionResourceDataDocument; - put( - doc: StructuredDocument - ): ResourceMetaDocument | ResourceErrorDocument; - put(doc: StructuredDocument): ResourceDocument { - if ('content' in doc && !('error' in doc)) { - if (Array.isArray(doc.content.data)) { - const data = doc.content.data.map((data) => { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier(data); - this.upsert(identifier, data, this._storeWrapper.hasRecord(identifier)); - return identifier; - }); - return { data }; - } else { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( - doc.content.data as RecordIdentifier - ); - this.upsert(identifier, doc.content.data as JsonApiResource, this._storeWrapper.hasRecord(identifier)); - return { data: identifier } as SingleResourceDataDocument; - } - } else if ('error' in doc) { - throw typeof doc.error === 'string' ? new Error(doc.error) : doc.error; - } - throw new Error('Not Implemented'); - } - - peek(identifier: StableRecordIdentifier): ResourceBlob | null; - peek(identifier: StableDocumentIdentifier): ResourceDocument | null; - peek(identifier: StableDocumentIdentifier | StableRecordIdentifier): ResourceBlob | ResourceDocument | null { - throw new Error(`Not Implemented`); - } - peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null { - throw new Error(`Not Implemented`); - } - fork(): Promise { - throw new Error(`Not Implemented`); - } - merge(cache: Cache): Promise { - throw new Error(`Not Implemented`); - } - diff(): Promise { - throw new Error(`Not Implemented`); - } - dump(): Promise> { - throw new Error(`Not Implemented`); - } - hydrate(stream: ReadableStream): Promise { - throw new Error('Not Implemented'); - } - - upsert( - identifier: StableRecordIdentifier, - data: JsonApiResource, - calculateChanges?: boolean | undefined - ): void | string[] { - if (!this._data.has(identifier)) { - this._storeWrapper.notifyChange(identifier, 'added'); - } - this._data.set(identifier, data); - this._storeWrapper.notifyChange(identifier, 'attributes'); - this._storeWrapper.notifyChange(identifier, 'relationships'); - } - - clientDidCreate(identifier: StableRecordIdentifier, options?: Dict | undefined): Dict { - this._isNew = true; - return {}; - } - willCommit(identifier: StableRecordIdentifier): void {} - didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument { - return { data: identifier as StableExistingRecordIdentifier }; - } - commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiError[] | undefined): void { - this._errors = errors; - } - unloadRecord(identifier: StableRecordIdentifier): void {} - getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - return ''; - } - setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - throw new Error('Method not implemented.'); - } - changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - return {}; - } - hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - return false; - } - rollbackAttrs(identifier: StableRecordIdentifier): string[] { - return []; - } - getRelationship( - identifier: StableRecordIdentifier, - propertyName: string - ): SingleResourceRelationship | CollectionResourceRelationship { - throw new Error('Method not implemented.'); - } - mutate(operation: LocalRelationshipOperation): void { - throw new Error('Method not implemented.'); - } - setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - throw new Error('Method not implemented.'); - } - - getErrors(identifier: StableRecordIdentifier): JsonApiError[] { - return this._errors || []; - } - isEmpty(identifier: StableRecordIdentifier): boolean { - return false; - } - isNew(identifier: StableRecordIdentifier): boolean { - return this._isNew; - } - isDeleted(identifier: StableRecordIdentifier): boolean { - return false; - } - isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return false; - } -} - -const TestRecordData: typeof V2TestRecordData | typeof V1TestRecordData = !DEPRECATE_V1_RECORD_DATA - ? V2TestRecordData - : V1TestRecordData; - -class CustomStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new TestRecordData(storeWrapper, identifier); - } -} - -module('integration/record-data - Custom RecordData Implementations', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - let { owner } = this; - - owner.register('model:person', Person); - owner.register('model:house', House); - owner.unregister('service:store'); - owner.register('service:store', CustomStore); - owner.register('adapter:application', JSONAPIAdapter.extend()); - owner.register('serializer:application', class extends JSONAPISerializer {}); - }); - - test('A RecordData implementation that has the required spec methods should not error out', async function (assert) { - const { owner } = this; - const store: Store = owner.lookup('service:store') as Store; - - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }, - { - type: 'person', - id: '2', - attributes: { - name: 'Scumbag Katz', - }, - }, - ], - }); - - let all = store.peekAll('person'); - assert.strictEqual(all.length, 2, 'we have 2 records'); - - store.push({ - data: [ - { - type: 'person', - id: '3', - attributes: { - name: 'Scumbag Bryn', - }, - }, - ], - }); - - await settled(); - - assert.strictEqual(all.length, 3, 'we have 3 records'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 6 }); - } - }); - - test('Record Data push, create and save lifecycle', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 20 : 19); - let called = 0; - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - let { owner } = this; - let calledUpsert = 0; - let calledClientDidCreate = 0; - let calledWillCommit = 0; - let calledWasRejected = 0; - let calledUnloadRecord = 0; - let calledRollbackAttributes = 0; - let calledDidCommit = 0; - let isNew = false; - - class LifecycleRecordData extends TestRecordData { - pushData(data: object, calculateChange: true): string[]; - pushData(data: object, calculateChange?: false): void; - pushData(data: object, calculateChange?: boolean): string[] | void { - if (DEPRECATE_V1_RECORD_DATA) { - calledUpsert++; - } else { - throw new Error(`Unexpected pushData call`); - } - } - - upsert() { - if (DEPRECATE_V1_RECORD_DATA) { - throw new Error(`Unexpected upsert call`); - } - calledUpsert++; - } - - clientDidCreate() { - calledClientDidCreate++; - isNew = true; - } - - willCommit() { - calledWillCommit++; - } - - commitWasRejected(identifier, errors) { - super.commitWasRejected(identifier, errors); - calledWasRejected++; - } - - unloadRecord() { - calledUnloadRecord++; - } - - rollbackAttrs() { - calledRollbackAttributes++; - } - rollbackAttributes() { - calledRollbackAttributes++; - } - - didCommit(identifier, result) { - calledDidCommit++; - isNew = false; - return { data: identifier }; - } - - isNew() { - return isNew; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new LifecycleRecordData(storeWrapper, identifier); - } - createCache(storeWrapper: CacheStoreWrapper) { - // @ts-expect-error - return new LifecycleRecordData(storeWrapper) as Cache; - } - } - - let TestAdapter = EmberObject.extend({ - updateRecord() { - called++; - if (called === 1) { - return Promise.resolve(); - } else if (called > 1) { - return Promise.reject(); - } - }, - - createRecord() { - return Promise.resolve(); - }, - }); - - owner.register('service:store', TestStore); - owner.register('adapter:application', TestAdapter, { singleton: false }); - - const store = owner.lookup('service:store') as Store; - - store.push({ - data: [personHash], - }); - assert.strictEqual(calledUpsert, 1, 'Called upsert'); - - let person = store.peekRecord('person', '1') as DSModel; - person.save(); - assert.strictEqual(calledWillCommit, 1, 'Called willCommit'); - - await settled(); - assert.strictEqual(calledDidCommit, 1, 'Called didCommit'); - - let promise = person.save(); - assert.strictEqual(calledWillCommit, 2, 'Called willCommit'); - - await promise.catch((_e) => assert.ok(true, 'we erred')); - - assert.strictEqual(calledDidCommit, 1, 'Did not call didCommit again'); - assert.strictEqual(calledWasRejected, 1, 'Called commitWasRejected'); - - person.rollbackAttributes(); - assert.strictEqual(calledRollbackAttributes, 1, 'Called rollbackAttributes'); - - person.unloadRecord(); - assert.strictEqual(calledUnloadRecord, 1, 'Called unloadRecord'); - - await settled(); - assert.strictEqual(calledClientDidCreate, 0, 'Did not called clientDidCreate'); - - calledUpsert = 0; - calledClientDidCreate = 0; - calledWillCommit = 0; - calledWasRejected = 0; - calledUnloadRecord = 0; - calledRollbackAttributes = 0; - calledDidCommit = 0; - - let clientPerson: DSModel = store.createRecord('person', { id: '2' }) as DSModel; - assert.strictEqual(calledClientDidCreate, 1, 'Called clientDidCreate'); - - clientPerson.save(); - assert.strictEqual(calledWillCommit, 1, 'Called willCommit'); - - await settled(); - assert.strictEqual(calledDidCommit, 1, 'Called didCommit'); - - promise = clientPerson.save(); - assert.strictEqual(calledWillCommit, 2, 'Called willCommit'); - - await promise.catch((_e) => assert.ok('we erred')); - assert.strictEqual(calledWasRejected, 1, 'Called commitWasRejected'); - assert.strictEqual(calledDidCommit, 1, 'Did not call didCommit again'); - - clientPerson.unloadRecord(); - assert.strictEqual(calledUnloadRecord, 1, 'Called unloadRecord'); - - await settled(); - assert.strictEqual(calledUpsert, 0, 'Did not call pushData'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 4 }); - } - }); - - test('Record Data attribute setting', async function (assert) { - let expectedCount = DEPRECATE_V1_RECORD_DATA ? 14 : 13; - assert.expect(expectedCount); - const personHash = { - type: 'person', - id: '1', - attributes: { - name: 'Scumbag Dale', - }, - }; - - let { owner } = this; - let calledGet = 0; - - class AttributeRecordData extends TestRecordData { - changedAttributes(): any { - return { name: ['old', 'new'] }; - } - - hasChangedAttributes(): boolean { - return false; - } - - changedAttrs(): any { - return { name: ['old', 'new'] }; - } - - hasChangedAttrs(): boolean { - return false; - } - - setAttr(identifier: StableRecordIdentifier, key: string, value: any) { - assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); - assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); - } - - setDirtyAttribute(key: string, value: any) { - assert.strictEqual(key, 'name', 'key passed to setDirtyAttribute'); - assert.strictEqual(value, 'new value', 'value passed to setDirtyAttribute'); - } - - getAttr(identifier: StableRecordIdentifier, key: string): string { - calledGet++; - if (!DEPRECATE_V1_RECORD_DATA) { - assert.strictEqual(key, 'name', 'key passed to getAttr'); - } else { - assert.strictEqual(identifier as unknown as string, 'name', 'key passed to getAttr'); - } - return 'new attribute'; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new AttributeRecordData(storeWrapper, identifier); - } - - createCache(storeWrapper: CacheStoreWrapper) { - // @ts-expect-error - return new AttributeRecordData(storeWrapper) as Cache; - } - } - - owner.register('service:store', TestStore); - - const store = owner.lookup('service:store') as Store; - - store.push({ - data: [personHash], - }); - - let person = store.peekRecord('person', '1') as DSModel; - assert.strictEqual(person.name, 'new attribute'); - assert.strictEqual(calledGet, 1, 'called getAttr for initial get'); - person.set('name', 'new value'); - assert.strictEqual(calledGet, 2, 'called getAttr during set'); - assert.strictEqual(person.name, 'new value'); - assert.strictEqual(calledGet, 2, 'did not call getAttr after set'); - person.notifyPropertyChange('name'); - assert.strictEqual(person.name, 'new attribute'); - assert.strictEqual(calledGet, 3, 'called getAttr after notifyPropertyChange'); - assert.deepEqual( - person.changedAttributes(), - { name: ['old', 'new'] }, - 'changed attributes passes through RD value' - ); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - }); -}); diff --git a/tests/main/tests/integration/record-data/store-wrapper-test.ts b/tests/main/tests/integration/record-data/store-wrapper-test.ts deleted file mode 100644 index 0aa5abc1c19..00000000000 --- a/tests/main/tests/integration/record-data/store-wrapper-test.ts +++ /dev/null @@ -1,428 +0,0 @@ -import { settled } from '@ember/test-helpers'; - -import { module, test } from 'qunit'; - -import Store from 'ember-data/store'; -import { setupTest } from 'ember-qunit'; - -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { recordIdentifierFor } from '@ember-data/store'; -import { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; -import { DSModel } from '@ember-data/types/q/ds-model'; -import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import publicProps from '@ember-data/unpublished-test-infra/test-support/public-props'; - -class Person extends Model { - @attr('string', {}) - name; -} - -class Car extends Model { - @belongsTo('house', { async: true, inverse: 'car' }) - garage; - - @attr('string', {}) - make; -} - -class House extends Model { - @attr('string', {}) - name; - - @belongsTo('person', { async: false, inverse: null }) - landlord; - - @belongsTo('car', { async: false, inverse: 'garage' }) - car; - - @hasMany('person', { async: false, inverse: null }) - tenants; -} - -// TODO: this should work -// class TestRecordData implements RecordData -class TestRecordData { - _isNew = false; - pushData(data, calculateChange?: boolean) {} - upsert() {} - clientDidCreate() { - this._isNew = true; - } - - willCommit() {} - - commitWasRejected() {} - - isDeletionCommitted() { - return false; - } - isDeleted() { - return false; - } - - unloadRecord() {} - rollbackAttributes() {} - changedAttributes(): any {} - - hasChangedAttributes(): boolean { - return false; - } - - setDirtyAttribute(key: string, value: any) {} - - getAttr(identifier: StableRecordIdentifier, key: string): unknown { - return 'test'; - } - - getHasMany(key: string) { - return {}; - } - - addToHasMany(key: string, recordDatas: this[], idx?: number) {} - removeFromHasMany(key: string, recordDatas: this[]) {} - setDirtyHasMany(key: string, recordDatas: this[]) {} - - getBelongsTo(key: string) {} - - setDirtyBelongsTo(name: string, recordData: this | null) {} - - didCommit(data) {} - - isNew() { - return this._isNew; - } - - isEmpty() { - return false; - } - - _initRecordCreateOptions(options) {} -} - -class CustomStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - return new TestRecordData(); - } -} - -let houseHash, houseHash2; - -module('integration/store-wrapper - RecordData StoreWrapper tests', function (hooks) { - setupTest(hooks); - - hooks.beforeEach(function () { - let { owner } = this; - houseHash = { - type: 'house', - id: '1', - attributes: { - name: 'Moomin', - }, - }; - - houseHash2 = { - type: 'house', - id: '2', - attributes: { - name: 'Lodge', - }, - }; - - owner.register('model:person', Person); - owner.register('model:house', House); - owner.register('model:car', Car); - owner.unregister('service:store'); - owner.register('service:store', CustomStore); - }); - - test('Relationship definitions', async function (assert) { - const { owner } = this; - let storeWrapper!: CacheStoreWrapper; - - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - storeWrapper = wrapper; - return super.createCache(wrapper); - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - store.cache; - - let houseAttrs = { - name: { - type: 'string', - isAttribute: true, - options: {}, - name: 'name', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'house' }), - houseAttrs, - 'can lookup attribute definitions for self' - ); - - let carAttrs = { - make: { - type: 'string', - isAttribute: true, - options: {}, - name: 'make', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'car' }), - carAttrs, - 'can lookup attribute definitions for other models' - ); - - let houseRelationships = { - landlord: { - key: 'landlord', - kind: 'belongsTo', - name: 'landlord', - type: 'person', - options: { async: false, inverse: null }, - }, - car: { - key: 'car', - kind: 'belongsTo', - name: 'car', - type: 'car', - options: { async: false, inverse: 'garage' }, - }, - tenants: { - key: 'tenants', - kind: 'hasMany', - name: 'tenants', - options: { async: false, inverse: null }, - type: 'person', - }, - }; - let schema = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'house' }); - let result = publicProps(['key', 'kind', 'name', 'type', 'options'], schema); - - // Retrive only public values from the result - // This should go away once we put private things in symbols/weakmaps - assert.deepEqual(houseRelationships, result, 'can lookup relationship definitions'); - }); - - if (DEPRECATE_V1_RECORD_DATA) { - test('RecordDataFor', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - let { owner } = this; - - let count = 0; - class RecordDataForTest extends TestRecordData { - id: string; - - constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - super(); - count++; - this.id = identifier.id!; - - if (count === 1) { - const identifier = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); - assert.strictEqual( - storeWrapper.recordDataFor(identifier).getAttr(identifier, 'name'), - 'ours name', - 'Can lookup another RecordData that has been loaded' - ); - const identifier2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); - const cache = storeWrapper.recordDataFor(identifier2); - const attrValue = cache.getAttr(identifier2, 'name'); - assert.strictEqual(attrValue, 'Chris', 'Can lookup another RecordData which hasnt been loaded'); - } - } - - getAttr(identifier: StableRecordIdentifier, key: string): unknown { - return 'ours name'; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return this.cache; - } - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - - store.push({ - data: [{ id: '1', type: 'person', attributes: { name: 'Chris' } }, houseHash, houseHash2], - }); - - assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 5 }); - } - }); - - test('recordDataFor - create new', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - let { owner } = this; - let count = 0; - let cache; - let newRecordData; - let firstIdentifier, secondIdentifier; - - class RecordDataForTest extends TestRecordData { - id: string; - _isNew: boolean = false; - - constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - super(); - count++; - this.id = identifier.id!; - - if (count === 1) { - const newIdentifier = wrapper.identifierCache.createIdentifierForNewRecord({ type: 'house' }); - cache = wrapper.recordDataFor(newIdentifier); - firstIdentifier = newIdentifier; - cache.clientDidCreate(newIdentifier); - } else if (count === 2) { - newRecordData = this; - secondIdentifier = identifier; - } - } - - clientDidCreate() { - this._isNew = true; - } - - isNew() { - return this._isNew; - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return this.cache; - } - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - - store.push({ - data: { - type: 'house', - id: '1', - attributes: { - bedrooms: 1, - }, - }, - }); - - assert.ok(cache.isNew(firstIdentifier), 'Our RecordData is new'); - assert.ok( - newRecordData.isNew(secondIdentifier), - 'The recordData for a RecordData created via Wrapper.recordDataFor(type) is in the "new" state' - ); - - assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 4 }); - } - }); - } - - test('setRecordId', async function (assert) { - const { owner } = this; - let storeWrapper!: CacheStoreWrapper; - - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - storeWrapper = wrapper; - return super.createCache(wrapper); - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - - let house = store.createRecord('house', {}) as DSModel; - storeWrapper.setRecordId(recordIdentifierFor(house), '17'); - assert.strictEqual(house.id, '17', 'setRecordId correctly set the id'); - assert.strictEqual( - store.peekRecord('house', '17'), - house, - 'can lookup the record from the identify map based on the new id' - ); - }); - - test('hasRecord', async function (assert) { - const { owner } = this; - - let storeWrapper!: CacheStoreWrapper; - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - storeWrapper = wrapper; - return super.createCache(wrapper); - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - - store.push({ - data: [houseHash, houseHash2], - }); - store.peekRecord('house', '1'); - - // TODO isRecordInUse returns true if record has never been instantiated, think through whether thats correct - let house2 = store.peekRecord('house', '2') as DSModel; - house2.unloadRecord(); - - store.createRecord('house', {}); - const id1 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '1' }); - const id2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); - assert.true(storeWrapper.hasRecord(id1), 'house 1 is in use'); - assert.false(storeWrapper.hasRecord(id2), 'house 2 is not in use'); - }); - - test('disconnectRecord', async function (assert) { - const { owner } = this; - - let storeWrapper!: CacheStoreWrapper; - class TestStore extends Store { - createCache(wrapper: CacheStoreWrapper) { - storeWrapper = wrapper; - return super.createCache(wrapper); - } - } - - owner.register('service:store', TestStore); - const store = owner.lookup('service:store') as Store; - - const identifier = store._push({ - data: { - type: 'house', - id: '1', - attributes: { - name: 'Moomin', - }, - }, - }); - storeWrapper.disconnectRecord(identifier as StableRecordIdentifier); - await settled(); - assert.strictEqual(store.peekRecord('house', '1'), null, 'record was removed from id map'); - }); -}); diff --git a/tests/main/tests/integration/record-data/unloading-record-data-test.js b/tests/main/tests/integration/record-data/unloading-record-data-test.js deleted file mode 100644 index 7e1498fe237..00000000000 --- a/tests/main/tests/integration/record-data/unloading-record-data-test.js +++ /dev/null @@ -1,262 +0,0 @@ -import { settled } from '@ember/test-helpers'; - -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - -class Person extends Model { - @hasMany('pet', { inverse: null, async: false }) - pets; - @attr() - name; -} - -class Pet extends Model { - @belongsTo('person', { inverse: null, async: false }) - owner; - @attr() - name; -} - -module('RecordData Compatibility', function (hooks) { - let store; - setupTest(hooks); - - hooks.beforeEach(function () { - let { owner } = this; - owner.register('model:person', Person); - owner.register('model:pet', Pet); - store = owner.lookup('service:store'); - }); - - class V1CustomRecordData { - constructor(identifier, storeWrapper) { - this.type = identifier.type; - this.id = identifier.id || null; - this.clientId = identifier.lid; - this.storeWrapper = storeWrapper; - this.attributes = null; - this.relationships = null; - } - - pushData(jsonApiResource, shouldCalculateChanges) { - let oldAttrs = this.attributes; - let changedKeys; - - this.attributes = jsonApiResource.attributes || null; - - if (shouldCalculateChanges) { - changedKeys = Object.keys(Object.assign({}, oldAttrs, this.attributes)); - } - - return changedKeys || []; - } - - getAttr(member) { - return this.attributes !== null ? this.attributes[member] : undefined; - } - - // TODO missing from RFC but required to implement - _initRecordCreateOptions(options) { - return options !== undefined ? options : {}; - } - // TODO missing from RFC but required to implement - getResourceIdentifier() { - return { - id: this.id, - type: this.type, - clientId: this.clientId, - }; - } - isEmpty() { - return false; - } - // TODO missing from RFC but required to implement - unloadRecord() { - this.attributes = null; - this.relationships = null; - } - // TODO missing from RFC but required to implement - isNew() { - return this.id === null; - } - isDeleted() { - return false; - } - isDeletionCommitted() { - return false; - } - - adapterDidCommit() {} - didCreateLocally() {} - adapterWillCommit() {} - saveWasRejected() {} - adapterDidDelete() {} - recordUnloaded() {} - rollbackAttributes() {} - rollbackAttribute() {} - changedAttributes() {} - hasChangedAttributes() {} - setAttr() {} - getHasMany() {} - addToHasMany() {} - removeFromHasMany() {} - getBelongsTo() {} - } - class V2CustomRecordData { - version = '2'; - constructor(identifier, storeWrapper) { - this.type = identifier.type; - this.id = identifier.id || null; - this.clientId = identifier.lid; - this.storeWrapper = storeWrapper; - this.attributes = null; - this.relationships = null; - } - - upsert(identifier, jsonApiResource, shouldCalculateChanges) { - let oldAttrs = this.attributes; - let changedKeys; - - this.attributes = jsonApiResource.attributes || null; - - if (shouldCalculateChanges) { - changedKeys = Object.keys(Object.assign({}, oldAttrs, this.attributes)); - } - - return changedKeys || []; - } - - getAttr(identifier, member) { - return this.attributes !== null ? this.attributes[member] : undefined; - } - - clientDidCreate(options) { - return options !== undefined ? options : {}; - } - isEmpty() { - return false; - } - unloadRecord() { - this.attributes = null; - this.relationships = null; - } - isNew() { - return this.id === null; - } - isDeleted() { - return false; - } - isDeletionCommitted() { - return false; - } - - adapterDidCommit() {} - didCreateLocally() {} - adapterWillCommit() {} - saveWasRejected() {} - adapterDidDelete() {} - recordUnloaded() {} - rollbackAttributes() {} - rollbackAttribute() {} - changedAttributes() {} - hasChangedAttributes() {} - setAttr() {} - update() {} - getRelationship() {} - } - - const CustomRecordData = DEPRECATE_V1_RECORD_DATA ? V1CustomRecordData : V2CustomRecordData; - - if (DEPRECATE_V1_RECORD_DATA) { - test(`store.unloadRecord on a record with default RecordData with relationship to a record with custom RecordData does not error`, async function (assert) { - let customCalled = 0, - customCalledFor = [], - originalCalled = 0, - originalCalledFor = []; - store.createRecordDataFor = function provideCustomRecordData(identifier, storeWrapper) { - if (identifier.type === 'pet') { - customCalled++; - customCalledFor.push(identifier); - return new CustomRecordData(identifier, storeWrapper); - } else { - originalCalled++; - originalCalledFor.push(identifier); - return this.cache; - } - }; - - let chris = store.push({ - data: { - type: 'person', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '2' }, - ], - }, - }, - }, - included: [ - { - type: 'pet', - id: '1', - attributes: { name: 'Shen' }, - relationships: { - owner: { data: { type: 'person', id: '1' } }, - }, - }, - { - type: 'pet', - id: '2', - attributes: { name: 'Prince' }, - relationships: { - owner: { data: { type: 'person', id: '1' } }, - }, - }, - ], - }); - let pets = chris.pets; - let shen = pets.at(0); - - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 5 }); - } - - assert.strictEqual(shen.name, 'Shen', 'We found Shen'); - assert.strictEqual(customCalled, 2, 'we used the custom record-data for pet'); - assert.deepEqual( - customCalledFor.map((i) => { - return { type: i.type, id: i.id }; - }), - [ - { id: '1', type: 'pet' }, - { id: '2', type: 'pet' }, - ], - 'we used the cutom record-data for the correct pets' - ); - assert.strictEqual(originalCalled, 1, 'we used the default record-data for person'); - assert.deepEqual( - originalCalledFor.map((i) => { - return { type: i.type, id: i.id }; - }), - [{ id: '1', type: 'person' }], - 'we used the default record-data for the correct person' - ); - - try { - chris.unloadRecord(); - await settled(); - assert.ok(true, 'expected `unloadRecord()` not to throw'); - } catch (e) { - assert.ok(false, 'expected `unloadRecord()` not to throw'); - } - }); - } -}); diff --git a/tests/main/tests/integration/records/collection-save-test.js b/tests/main/tests/integration/records/collection-save-test.js index c977540b970..b655405a254 100644 --- a/tests/main/tests/integration/records/collection-save-test.js +++ b/tests/main/tests/integration/records/collection-save-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -23,18 +22,18 @@ module('integration/records/collection_save - Save Collection of Records', funct test('Collection will resolve save on success', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let id = 1; store.createRecord('post', { title: 'Hello' }); store.createRecord('post', { title: 'World' }); - let posts = store.peekAll('post'); + const posts = store.peekAll('post'); adapter.createRecord = function (store, type, snapshot) { - return resolve({ data: { id: id++, type: 'post' } }); + return Promise.resolve({ data: { id: id++, type: 'post' } }); }; await posts.save().then(() => { @@ -43,16 +42,16 @@ module('integration/records/collection_save - Save Collection of Records', funct }); test('Collection will reject save on error', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.createRecord('post', { title: 'Hello' }); store.createRecord('post', { title: 'World' }); - let posts = store.peekAll('post'); + const posts = store.peekAll('post'); adapter.createRecord = function (store, type, snapshot) { - return reject(); + return Promise.reject(); }; try { @@ -64,27 +63,27 @@ module('integration/records/collection_save - Save Collection of Records', funct }); test('Retry is allowed in a failure handler', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.createRecord('post', { title: 'Hello' }); store.createRecord('post', { title: 'World' }); - let posts = store.peekAll('post'); + const posts = store.peekAll('post'); let count = 0; let id = 1; adapter.createRecord = function (store, type, snapshot) { if (count++ === 0) { - return reject(); + return Promise.reject(); } else { - return resolve({ data: { id: id++, type: 'post' } }); + return Promise.resolve({ data: { id: id++, type: 'post' } }); } }; adapter.updateRecord = function (store, type, snapshot) { - return resolve({ data: { id: snapshot.id, type: 'post' } }); + return Promise.resolve({ data: { id: snapshot.id, type: 'post' } }); }; await posts @@ -100,16 +99,16 @@ module('integration/records/collection_save - Save Collection of Records', funct test('Collection will reject save on invalid', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.createRecord('post', { title: 'Hello' }); store.createRecord('post', { title: 'World' }); - let posts = store.peekAll('post'); + const posts = store.peekAll('post'); adapter.createRecord = function (store, type, snapshot) { - return reject({ title: 'invalid' }); + return Promise.reject({ title: 'invalid' }); }; await posts.save().catch(() => { diff --git a/tests/main/tests/integration/records/concurrent-save-test.ts b/tests/main/tests/integration/records/concurrent-save-test.ts index 999d64ed82f..43eadb5012b 100644 --- a/tests/main/tests/integration/records/concurrent-save-test.ts +++ b/tests/main/tests/integration/records/concurrent-save-test.ts @@ -5,7 +5,7 @@ import { setupTest } from 'ember-qunit'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr } from '@ember-data/model'; import { createDeferred } from '@ember-data/request'; -import Store from '@ember-data/store'; +import type Store from '@ember-data/store'; module('Integration | Record | concurrent saves', function (hooks) { setupTest(hooks); @@ -59,7 +59,7 @@ module('Integration | Record | concurrent saves', function (hooks) { lastName: 'Huff-menne', }, }, - }) as unknown as User; + }) as User; user.firstName = 'Krystan'; resultPromises.push(user.save()); @@ -159,7 +159,7 @@ module('Integration | Record | concurrent saves', function (hooks) { lastName: 'Huff-menne', }, }, - }) as unknown as User; + }) as User; user.firstName = 'Krystan'; resultPromises.push(user.save()); @@ -270,7 +270,7 @@ module('Integration | Record | concurrent saves', function (hooks) { lastName: 'Huff-menne', }, }, - }) as unknown as User; + }) as User; user.firstName = 'Krystan'; resultPromises.push(user.save()); diff --git a/tests/main/tests/integration/records/create-record-test.js b/tests/main/tests/integration/records/create-record-test.js index e87870d68ad..1dc5dd95468 100644 --- a/tests/main/tests/integration/records/create-record-test.js +++ b/tests/main/tests/integration/records/create-record-test.js @@ -1,7 +1,6 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -32,7 +31,7 @@ module('Store.createRecord() coverage', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('model:pet', Pet); store = owner.lookup('service:store'); @@ -58,7 +57,7 @@ module('Store.createRecord() coverage', function (hooks) { }); test('unloading a newly created a record with a sync belongsTo relationship', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -73,7 +72,7 @@ module('Store.createRecord() coverage', function (hooks) { }, }); - let pet = store.createRecord('pet', { + const pet = store.createRecord('pet', { name: 'Shen', owner: chris, }); @@ -93,7 +92,7 @@ module('Store.createRecord() coverage', function (hooks) { }); test('unloading a record with a sync hasMany relationship to a newly created record', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -108,7 +107,7 @@ module('Store.createRecord() coverage', function (hooks) { }, }); - let pet = store.createRecord('pet', { + const pet = store.createRecord('pet', { name: 'Shen', owner: chris, }); @@ -148,7 +147,7 @@ module('Store.createRecord() coverage', function (hooks) { assert.ok(false, 'Adapter should not make any findBelongsTo Requests'); }, createRecord() { - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '2', @@ -165,7 +164,7 @@ module('Store.createRecord() coverage', function (hooks) { }) ); - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -181,7 +180,7 @@ module('Store.createRecord() coverage', function (hooks) { }, }); - let shen = store.createRecord('pet', { + const shen = store.createRecord('pet', { name: 'Shen', bestHuman: chris, }); diff --git a/tests/main/tests/integration/records/delete-record-test.js b/tests/main/tests/integration/records/delete-record-test.js index 31906791b32..3ec0190acc9 100644 --- a/tests/main/tests/integration/records/delete-record-test.js +++ b/tests/main/tests/integration/records/delete-record-test.js @@ -1,20 +1,16 @@ -/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "(adam|dave|cersei)" }]*/ - import EmberObject, { get } from '@ember/object'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { all, Promise as EmberPromise } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import { InvalidError } from '@ember-data/adapter/error'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; -import { DEBUG } from '@ember-data/env'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import { recordIdentifierFor } from '@ember-data/store'; +import { DEBUG } from '@warp-drive/build-config/env'; module('integration/deletedRecord - Deleting Records', function (hooks) { setupTest(hooks); @@ -54,11 +50,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); test('records should not be removed from record arrays just after deleting, but only after committing them', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function () { - return EmberPromise.resolve(); + return Promise.resolve(); }; store.push({ @@ -79,8 +75,8 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }, ], }); - let adam = store.peekRecord('person', 1); - let all = store.peekAll('person'); + const adam = store.peekRecord('person', 1); + const all = store.peekAll('person'); // pre-condition assert.strictEqual(all.length, 2, 'pre-condition: 2 records in array'); @@ -104,11 +100,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { this.owner.register('model:group', Group); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function () { - return EmberPromise.resolve(); + return Promise.resolve(); }; store.push({ @@ -142,8 +138,8 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { ], }); - let group = store.peekRecord('group', '1'); - let person = store.peekRecord('person', '1'); + const group = store.peekRecord('group', '1'); + const person = store.peekRecord('person', '1'); // Sanity Check we are in the correct state. assert.strictEqual(group.people.length, 2, 'expected 2 related records before delete'); @@ -155,11 +151,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); test('records can be deleted during record array enumeration', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function () { - return EmberPromise.resolve(); + return Promise.resolve(); }; store.push({ @@ -199,11 +195,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); test('Deleting an invalid newly created record should remove it from the store', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function () { - return EmberPromise.reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -216,7 +212,7 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { ); }; - let record = store.createRecord('person', { name: 'pablobm' }); + const record = store.createRecord('person', { name: 'pablobm' }); // Invalidate the record to put it in the `root.loaded.created.invalid` state await record.save().catch(() => {}); @@ -228,8 +224,8 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { ); assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; record.deleteRecord(); @@ -238,15 +234,15 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); test('Destroying an invalid newly created record should remove it from the store', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function () { assert.fail("The adapter's deletedRecord method should not be called when the record was created locally."); }; adapter.createRecord = function () { - return EmberPromise.reject( + return Promise.reject( new InvalidError([ { title: 'Invalid Attribute', @@ -259,7 +255,7 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { ); }; - let record = store.createRecord('person', { name: 'pablobm' }); + const record = store.createRecord('person', { name: 'pablobm' }); // Invalidate the record to put it in the `root.loaded.created.invalid` state await record.save().catch(() => {}); @@ -271,8 +267,8 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { ); assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; await record.destroyRecord(); @@ -281,17 +277,14 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); test('Will resolve destroy and save in same loop', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - - let adam, dave; - let promises; + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); assert.expect(1); adapter.createRecord = function () { assert.ok(true, 'save operation resolves'); - return EmberPromise.resolve({ + return Promise.resolve({ data: { id: '123', type: 'person', @@ -299,17 +292,17 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }); }; - adam = store.createRecord('person', { name: 'Adam Sunderland' }); - dave = store.createRecord('person', { name: 'Dave Sunderland' }); + const adam = store.createRecord('person', { name: 'Adam Sunderland' }); + const dave = store.createRecord('person', { name: 'Dave Sunderland' }); - promises = [adam.destroyRecord(), dave.save()]; + const promises = [adam.destroyRecord(), dave.save()]; - await all(promises); + await Promise.all(promises); }); test('Calling save on a newly created then deleted record should not error', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function () { assert.fail('We should not call adapter.createRecord on save'); @@ -321,29 +314,24 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.fail('We should not call adapter.deleteRecord on save'); }; - let record = store.createRecord('person', { name: 'pablobm' }); + const record = store.createRecord('person', { name: 'pablobm' }); assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; record.deleteRecord(); assert.true(cache.isEmpty(identifier), 'We reached the correct persisted saved state'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); - assert.strictEqual( - store._instanceCache.peek({ identifier, bucket: 'resourceCache' }), - undefined, - 'The cache is destroyed' - ); await record.save(); }); test('Calling unloadRecord on a newly created then deleted record should not error', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function () { assert.fail('We should not call adapter.createRecord on save'); @@ -355,32 +343,24 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.fail('We should not call adapter.deleteRecord on save'); }; - let record = store.createRecord('person', { name: 'pablobm' }); + const record = store.createRecord('person', { name: 'pablobm' }); assert.strictEqual(get(store.peekAll('person'), 'length'), 1, 'The new person should be in the store'); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; record.deleteRecord(); await settled(); assert.true(cache.isEmpty(identifier), 'We reached the correct persisted saved state'); assert.strictEqual(get(store.peekAll('person'), 'length'), 0, 'The new person should be removed from the store'); - assert.strictEqual( - store._instanceCache.peek({ identifier, bucket: 'resourceCache' }), - undefined, - 'The cache is destroyed' - ); record.unloadRecord(); await settled(); }); test('Records with an async hasMany can be pushed again after they were destroyed on client side', async function (assert) { - let group; - let employee; - class Company extends Model { @attr('string') name; toString() { @@ -403,11 +383,11 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { this.owner.register('model:group', Group); this.owner.register('model:employee', Employee); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function () { - return EmberPromise.resolve(); + return Promise.resolve(); }; // Push the company as a long-lived record that will be referenced by the group @@ -452,10 +432,10 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { }; // Server push with the group and employee - store.push(jsonEmployee); - store.push(jsonGroup); + store.push(structuredClone(jsonEmployee)); + store.push(structuredClone(jsonGroup)); - group = store.peekRecord('group', '1'); + let group = store.peekRecord('group', '1'); const groupCompany = await group.company; // Sanity Check @@ -463,7 +443,7 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.strictEqual(groupCompany.name, 'Inc.', 'group belongs to our company'); assert.strictEqual(group.employees.length, 1, 'expected 1 related record before delete'); const employees = await group.employees; - employee = employees.at(0); + const employee = employees.at(0); assert.strictEqual(employee.name, 'Adam Sunderland', 'expected related records to be loaded'); await group.destroyRecord(); @@ -514,9 +494,8 @@ module('integration/deletedRecord - Deleting Records', function (hooks) { assert.true(company.isDeleted, 'isDeleted should be true'); assert.true(company.isDestroying, 'isDestroying should be true'); assert.true(company.isDestroyed, 'isDestroyed should be true'); - if (DEBUG) { - assert.strictEqual(company.id, undefined, 'id access should be safe'); + assert.strictEqual(company.id, null, 'id access should be safe'); } } catch (e) { assert.ok(false, `Should not throw an error, threw ${e.message}`); diff --git a/tests/main/tests/integration/records/edit-record-test.js b/tests/main/tests/integration/records/edit-record-test.js index 55804918ef0..c983046f089 100644 --- a/tests/main/tests/integration/records/edit-record-test.js +++ b/tests/main/tests/integration/records/edit-record-test.js @@ -27,7 +27,7 @@ module('Editing a Record', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('model:pet', Pet); store = owner.lookup('service:store'); @@ -110,7 +110,7 @@ module('Editing a Record', function (hooks) { }); test('Change parent relationship then unload original child', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -123,7 +123,7 @@ module('Editing a Record', function (hooks) { }, }); - let john = store.push({ + const john = store.push({ data: { id: '2', type: 'person', @@ -136,7 +136,7 @@ module('Editing a Record', function (hooks) { }, }); - let shen = store.push({ + const shen = store.push({ data: { id: '3', type: 'pet', @@ -152,7 +152,7 @@ module('Editing a Record', function (hooks) { }, }); - let rocky = store.push({ + const rocky = store.push({ data: { id: '4', type: 'pet', @@ -191,7 +191,7 @@ module('Editing a Record', function (hooks) { module('Simple relationship addition case', function () { module('Adding a sync belongsTo relationship to a record', function () { test('We can add to a record', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -204,7 +204,7 @@ module('Editing a Record', function (hooks) { }, }); - let pet = store.push({ + const pet = store.push({ data: { id: '1', type: 'pet', @@ -233,12 +233,12 @@ module('Editing a Record', function (hooks) { }); test('We can add a new record to a record', async function (assert) { - let chris = store.createRecord('person', { + const chris = store.createRecord('person', { name: 'Chris', pets: [], }); - let pet = store.push({ + const pet = store.push({ data: { id: '1', type: 'pet', @@ -267,12 +267,12 @@ module('Editing a Record', function (hooks) { }); test('We can add a new record to a new record', async function (assert) { - let chris = store.createRecord('person', { + const chris = store.createRecord('person', { name: 'Chris', pets: [], }); - let pet = store.createRecord('pet', { + const pet = store.createRecord('pet', { name: 'Shen', owner: null, }); @@ -293,7 +293,7 @@ module('Editing a Record', function (hooks) { }); test('We can add to a new record', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -306,7 +306,7 @@ module('Editing a Record', function (hooks) { }, }); - let pet = store.createRecord('pet', { + const pet = store.createRecord('pet', { name: 'Shen', owner: null, }); @@ -327,7 +327,7 @@ module('Editing a Record', function (hooks) { }); test('Change parent relationship and unload original parent', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -343,7 +343,7 @@ module('Editing a Record', function (hooks) { }, }); - let john = store.push({ + const john = store.push({ data: { id: '2', type: 'person', @@ -356,7 +356,7 @@ module('Editing a Record', function (hooks) { }, }); - let shen = store.push({ + const shen = store.push({ data: { id: '3', type: 'pet', @@ -372,7 +372,7 @@ module('Editing a Record', function (hooks) { }, }); - let rocky = store.push({ + const rocky = store.push({ data: { id: '4', type: 'pet', @@ -415,7 +415,7 @@ module('Editing a Record', function (hooks) { module('Adding an async belongsTo relationship to a record', function () { test('We can add to a record', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -428,7 +428,7 @@ module('Editing a Record', function (hooks) { }, }); - let james = store.push({ + const james = store.push({ data: { id: '1', type: 'person', @@ -459,7 +459,7 @@ module('Editing a Record', function (hooks) { }); test('We can add a new record to a record', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { id: '1', type: 'person', @@ -472,7 +472,7 @@ module('Editing a Record', function (hooks) { }, }); - let james = store.createRecord('person', { + const james = store.createRecord('person', { name: 'James', bestFriend: null, }); @@ -495,12 +495,12 @@ module('Editing a Record', function (hooks) { }); test('We can add a new record to a new record', async function (assert) { - let chris = store.createRecord('person', { + const chris = store.createRecord('person', { name: 'Chris', bestFriend: null, }); - let james = store.createRecord('person', { + const james = store.createRecord('person', { name: 'James', bestFriend: null, }); @@ -523,12 +523,12 @@ module('Editing a Record', function (hooks) { }); test('We can add to a new record', async function (assert) { - let chris = store.createRecord('person', { + const chris = store.createRecord('person', { name: 'Chris', bestFriend: null, }); - let james = store.push({ + const james = store.push({ data: { id: '1', type: 'person', diff --git a/tests/main/tests/integration/records/error-test.js b/tests/main/tests/integration/records/error-test.js index 6b5494bfdc7..c2ca0e07be3 100644 --- a/tests/main/tests/integration/records/error-test.js +++ b/tests/main/tests/integration/records/error-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import RSVP from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -22,7 +21,7 @@ module('integration/records/error', function (hooks) { this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -35,7 +34,7 @@ module('integration/records/error', function (hooks) { }, }); - let person = store.peekRecord('person', 'wat'); + const person = store.peekRecord('person', 'wat'); person.setProperties({ firstName: null, @@ -78,9 +77,9 @@ module('integration/records/error', function (hooks) { this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: 'wat', firstName: 'Yehuda', lastName: 'Katz', @@ -127,9 +126,9 @@ module('integration/records/error', function (hooks) { this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: 'wat', firstName: 'Yehuda', }); @@ -161,9 +160,9 @@ module('integration/records/error', function (hooks) { this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { + const person = store.createRecord('person', { id: 'wat', firstName: 'Yehuda', }); @@ -194,11 +193,11 @@ module('integration/records/error', function (hooks) { this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = () => { - return RSVP.reject( + return Promise.reject( new InvalidError([ { detail: 'Must be unique', @@ -216,8 +215,8 @@ module('integration/records/error', function (hooks) { try { person = await person.save(); - } catch (_error) { - let errors = person.errors; + } catch { + const errors = person.errors; assert.strictEqual(errors.length, 2, 'Adds two errors to the model'); assert.true(errors.has('firstName'), 'firstName is included in the errors object'); diff --git a/tests/main/tests/integration/records/load-test.js b/tests/main/tests/integration/records/load-test.js index 99ba2821d52..7a860e6583a 100644 --- a/tests/main/tests/integration/records/load-test.js +++ b/tests/main/tests/integration/records/load-test.js @@ -2,7 +2,6 @@ import EmberObject from '@ember/object'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -37,7 +36,7 @@ module('integration/load - Loading Records', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); store = owner.lookup('service:store'); }); @@ -127,7 +126,7 @@ module('integration/load - Loading Records', function (hooks) { 'adapter:application', JSONAPIAdapter.extend({ findRecord() { - return reject(); + return Promise.reject(); }, }) ); @@ -142,7 +141,7 @@ module('integration/load - Loading Records', function (hooks) { 'adapter:application', JSONAPIAdapter.extend({ findRecord() { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }, }) ); @@ -154,14 +153,14 @@ module('integration/load - Loading Records', function (hooks) { } catch (e) { assert.strictEqual( e.message, - `Assertion Failed: The 'findRecord' request for person:1 resolved indicating success but contained no primary data. To indicate a 404 not found you should either reject the promise returned by the adapter's findRecord method or throw a NotFoundError.`, + `The 'findRecord' request for person:1 resolved indicating success but contained no primary data. To indicate a 404 not found you should either reject the promise returned by the adapter's findRecord method or throw a NotFoundError.`, 'we throw a meaningful error' ); } }); test('Empty records remain in the empty state while data is being fetched', async function (assert) { - let payloads = [ + const payloads = [ { data: { type: 'person', @@ -216,13 +215,13 @@ module('integration/load - Loading Records', function (hooks) { 'adapter:application', JSONAPIAdapter.extend({ findRecord() { - let payload = payloads.shift(); + const payload = payloads.shift(); if (payload === undefined) { - return reject(new Error('Invalid Request')); + return Promise.reject(new Error('Invalid Request')); } - return resolve(payload); + return Promise.resolve(payload); }, }) ); @@ -235,36 +234,33 @@ module('integration/load - Loading Records', function (hooks) { }) ); - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); const instanceCache = store._instanceCache; - let cache = instanceCache.peek({ identifier, bucket: 'resourceCache' }); + let cache = store.cache; // test that our initial state is correct - assert.strictEqual(cache, undefined, 'We begin in the empty state'); assert.false(_isLoading(instanceCache, identifier), 'We have not triggered a load'); let recordPromise = store.findRecord('person', '1'); // test that during the initial load our state is correct - cache = instanceCache.peek({ identifier, bucket: 'resourceCache' }); - assert.strictEqual(cache, undefined, 'awaiting first fetch: We remain in the empty state'); assert.true(_isLoading(instanceCache, identifier), 'awaiting first fetch: We have now triggered a load'); - let record = await recordPromise; + const record = await recordPromise; // test that after the initial load our state is correct - cache = instanceCache.peek({ identifier, bucket: 'resourceCache' }); + cache = store.cache; assert.false(cache.isEmpty(identifier), 'after first fetch: We are no longer empty'); assert.false(_isLoading(instanceCache, identifier), 'after first fetch: We have loaded'); assert.false(record.isReloading, 'after first fetch: We are not reloading'); - let bestFriend = await record.bestFriend; - let trueBestFriend = await bestFriend.bestFriend; + const bestFriend = await record.bestFriend; + const trueBestFriend = await bestFriend.bestFriend; // shen is our retainer for the record we are testing // that ensures unloadRecord later in this test does not fully // discard the identifier - let shen = store.peekRecord('person', '2'); + const shen = store.peekRecord('person', '2'); assert.strictEqual(bestFriend, shen, 'Precond: bestFriend is correct'); assert.strictEqual(trueBestFriend, record, 'Precond: bestFriend of bestFriend is correct'); @@ -296,18 +292,13 @@ module('integration/load - Loading Records', function (hooks) { // test that during a reload-due-to-unload our state is correct // This requires a retainer (the async bestFriend relationship) assert.true(cache.isEmpty(identifier), 'awaiting second find: We remain empty'); - let newRecordData = instanceCache.peek({ identifier, bucket: 'resourceCache' }); - assert.strictEqual(newRecordData, undefined, 'We have no resource data during second find'); assert.true(_isLoading(instanceCache, identifier), 'awaiting second find: We are loading again'); assert.false(record.isReloading, 'awaiting second find: We are not reloading'); await recordPromise; // test that after the reload-due-to-unload our state is correct - newRecordData = instanceCache.peek({ identifier, bucket: 'resourceCache' }); assert.false(cache.isEmpty(identifier), 'after second find: Our resource data is no longer empty'); - - assert.false(newRecordData.isEmpty(identifier), 'after second find: We are no longer empty'); assert.false(_isLoading(instanceCache, identifier), 'after second find: We have loaded'); assert.false(record.isReloading, 'after second find: We are not reloading'); }); diff --git a/tests/main/tests/integration/records/new-record-unload-test.js b/tests/main/tests/integration/records/new-record-unload-test.js index f5a9f23548b..4020433e3c3 100644 --- a/tests/main/tests/integration/records/new-record-unload-test.js +++ b/tests/main/tests/integration/records/new-record-unload-test.js @@ -4,12 +4,13 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import Model, { attr, hasMany } from '@ember-data/model'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Person extends Model { @attr name; @hasMany('person', { async: true, inverse: 'friends' }) friends; + @belongsTo('person', { async: true, inverse: null }) bestFriend; } module('Integration | Records | New Record Unload', function (hooks) { @@ -35,10 +36,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -60,6 +61,77 @@ module('Integration | Records | New Record Unload', function (hooks) { assert.strictEqual(people.length, 1, 'precond - one person left in the store'); }); + test('Rolling Back Attributes on multiple New (related via async self-reflexive HasMany) Records unloads them safely', async function (assert) { + const store = this.owner.lookup('service:store'); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Matt = store.createRecord('person', { name: 'Matthew Seidel', friends: [Pat] }); + const friends = Matt.hasMany('friends').value(); + const people = store.peekAll('person'); + + assert.strictEqual(friends.length, 1, 'Matt has friends'); + assert.strictEqual(people.length, 2, 'precond - two people records in the store'); + assert.true(Matt.hasDirtyAttributes, 'precond - record has dirty attributes'); + assert.true(Matt.isNew, 'precond - record is new'); + assert.true(Pat.hasDirtyAttributes, 'precond - record has dirty attributes'); + assert.true(Pat.isNew, 'precond - record is new'); + + Pat.rollbackAttributes(); + + assert.false(Pat.isDestroyed, 'Pat record is not yet destroyed'); + assert.true(Pat.isDestroying, 'Pat record is destroying'); + assert.strictEqual(friends.length, 0, 'Matt has no friends'); + assert.strictEqual(people.length, 1, 'precond - one person left in the store'); + + Matt.rollbackAttributes(); + assert.false(Matt.isDestroyed, 'Matt record is not yet destroyed'); + assert.true(Matt.isDestroying, 'Matt record is destroying'); + assert.strictEqual(people.length, 0, 'precond - no people left in the store'); + + await settled(); + + assert.true(Pat.isDestroyed, 'Pat record is destroyed'); + assert.true(Pat.isDestroying, 'Pat record is destroying'); + assert.true(Matt.isDestroyed, 'Matt record is destroyed'); + assert.true(Matt.isDestroying, 'Matt record is destroying'); + assert.strictEqual(people.length, 0, 'precond - no people left in the store'); + }); + + test('Rolling Back Attributes on multiple New (related via async belongsTo with no inverse) Records unloads them safely', async function (assert) { + const store = this.owner.lookup('service:store'); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Matt = store.createRecord('person', { name: 'Matthew Seidel', bestFriend: Pat }); + let bestFriend = Matt.belongsTo('bestFriend').value(); + const people = store.peekAll('person'); + + assert.strictEqual(people.length, 2, 'precond - two people records in the store'); + assert.strictEqual(bestFriend, Pat, 'Matt has a best friend'); + assert.true(Matt.hasDirtyAttributes, 'precond - record has dirty attributes'); + assert.true(Matt.isNew, 'precond - record is new'); + assert.true(Pat.hasDirtyAttributes, 'precond - record has dirty attributes'); + assert.true(Pat.isNew, 'precond - record is new'); + + Pat.rollbackAttributes(); + + bestFriend = Matt.belongsTo('bestFriend').value(); + assert.strictEqual(bestFriend, null, 'Matt has no best friend'); + assert.false(Pat.isDestroyed, 'Pat record is not yet destroyed'); + assert.true(Pat.isDestroying, 'Pat record is destroying'); + assert.strictEqual(people.length, 1, 'precond - one person left in the store'); + + Matt.rollbackAttributes(); + assert.false(Matt.isDestroyed, 'Matt record is not yet destroyed'); + assert.true(Matt.isDestroying, 'Matt record is destroying'); + assert.strictEqual(people.length, 0, 'precond - no people left in the store'); + + await settled(); + + assert.true(Pat.isDestroyed, 'Pat record is destroyed'); + assert.true(Pat.isDestroying, 'Pat record is destroying'); + assert.true(Matt.isDestroyed, 'Matt record is destroyed'); + assert.true(Matt.isDestroying, 'Matt record is destroying'); + assert.strictEqual(people.length, 0, 'precond - no people left in the store'); + }); + test('Unload on a New Record unloads that record safely', async function (assert) { const store = this.owner.lookup('service:store'); const Matt = store.push({ @@ -76,10 +148,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -119,10 +191,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -132,7 +204,7 @@ module('Integration | Records | New Record Unload', function (hooks) { try { await Pat.save(); assert.ok(false, 'save failed'); - } catch (e) { + } catch { assert.ok(true, 'save failed'); } @@ -169,10 +241,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -182,7 +254,7 @@ module('Integration | Records | New Record Unload', function (hooks) { try { await Pat.save(); assert.ok(false, 'save failed'); - } catch (e) { + } catch { assert.ok(true, 'save failed'); } @@ -219,10 +291,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -232,7 +304,7 @@ module('Integration | Records | New Record Unload', function (hooks) { try { await Pat.save(); assert.ok(false, 'save failed'); - } catch (e) { + } catch { assert.ok(true, 'save failed'); } @@ -269,10 +341,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -282,7 +354,7 @@ module('Integration | Records | New Record Unload', function (hooks) { try { await Pat.save(); assert.ok(true, 'save succeeded'); - } catch (e) { + } catch { assert.ok(false, 'save succeeded'); } @@ -324,10 +396,10 @@ module('Integration | Records | New Record Unload', function (hooks) { }, }, }); - let Pat = store.createRecord('person', { name: 'Patrick Wachter' }); + const Pat = store.createRecord('person', { name: 'Patrick Wachter' }); const friends = Matt.hasMany('friends').value(); friends.push(Pat); - let people = store.peekAll('person'); + const people = store.peekAll('person'); assert.strictEqual(friends.length, 1, 'Matt has friends'); assert.strictEqual(people.length, 2, 'precond - two people records in the store'); @@ -337,7 +409,7 @@ module('Integration | Records | New Record Unload', function (hooks) { try { await Pat.save(); assert.ok(true, 'save succeeded'); - } catch (e) { + } catch { assert.ok(false, 'save succeeded'); } diff --git a/tests/main/tests/integration/records/polymorphic-find-record-test.ts b/tests/main/tests/integration/records/polymorphic-find-record-test.ts new file mode 100644 index 00000000000..1c52de5e36a --- /dev/null +++ b/tests/main/tests/integration/records/polymorphic-find-record-test.ts @@ -0,0 +1,53 @@ +import { settled } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import type Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; + +class Person extends Model { + @attr declare name: string; +} +class Employee extends Person {} + +module('integration/records/polymorphic-find-record - Polymorphic findRecord', function (hooks) { + setupTest(hooks); + + test('when findRecord with abstract type returns concrete type', async function (assert) { + this.owner.register('model:person', Person); + this.owner.register('model:employee', Employee); + + const store = this.owner.lookup('service:store') as Store; + const adapter = store.adapterFor('application'); + + adapter.findRecord = () => { + return Promise.resolve({ + data: { + id: '1', + type: 'employee', + attributes: { + name: 'Rey Skybarker', + }, + }, + }); + }; + + const person = (await store.findRecord('person', '1')) as Employee; + assert.ok(person instanceof Employee, 'record is an instance of Employee'); + assert.strictEqual(person.name, 'Rey Skybarker', 'name is correct'); + assert.strictEqual(recordIdentifierFor(person).type, 'employee', 'identifier has the concrete type'); + + const employee = store.peekRecord('employee', '1'); + const person2 = store.peekRecord('person', '1'); + assert.strictEqual(employee, person, 'peekRecord returns the same instance for concrete type'); + assert.strictEqual(person2, person, 'peekRecord returns the same instance for abstract type'); + assert.strictEqual(store.identifierCache._cache.resources.size, 2, 'identifier cache contains backreferences'); + + person.unloadRecord(); + await settled(); + assert.strictEqual(store.identifierCache._cache.resources.size, 0, 'identifier cache is empty'); + }); +}); diff --git a/tests/main/tests/integration/records/property-changes-test.js b/tests/main/tests/integration/records/property-changes-test.js index e1e8b41dfd6..e4a735b2e5c 100644 --- a/tests/main/tests/integration/records/property-changes-test.js +++ b/tests/main/tests/integration/records/property-changes-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -24,7 +23,7 @@ module('integration/records/property-changes - Property changes', function (hook test('Calling push with partial records trigger observers for just those attributes that changed', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -61,7 +60,7 @@ module('integration/records/property-changes - Property changes', function (hook test('Calling push does not trigger observers for locally changed attributes with the same value', async function (assert) { assert.expect(0); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var person; @@ -102,11 +101,11 @@ module('integration/records/property-changes - Property changes', function (hook assert.expect(1); var person; - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (store, type, snapshot) { - return resolve({ data: { id: 'wat', type: 'person', attributes: { 'last-name': 'Katz' } } }); + return Promise.resolve({ data: { id: 'wat', type: 'person', attributes: { 'last-name': 'Katz' } } }); }; store.push({ @@ -136,7 +135,7 @@ module('integration/records/property-changes - Property changes', function (hook test('store.push should not override a modified attribute', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const person = store.push({ data: { diff --git a/tests/main/tests/integration/records/relationship-changes-test.js b/tests/main/tests/integration/records/relationship-changes-test.js index a5554934c6e..c6f9ebcd1fc 100644 --- a/tests/main/tests/integration/records/relationship-changes-test.js +++ b/tests/main/tests/integration/records/relationship-changes-test.js @@ -1,6 +1,5 @@ -import EmberObject, { get, set } from '@ember/object'; +import EmberObject from '@ember/object'; import { alias } from '@ember/object/computed'; -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -68,59 +67,54 @@ module('integration/records/relationship-changes - Relationship changes', functi 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: 1 }, + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }, function (assert) { - assert.expect(1); + assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let Obj = EmberObject.extend({ + const Obj = EmberObject.extend({ person: null, siblings: alias('person.siblings'), }); const obj = Obj.create(); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [], - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], }, }, - }); - set(obj, 'person', store.peekRecord('person', 'wat')); + }, }); + obj.person = store.peekRecord('person', 'wat'); + assert.arrayStrictEquals(obj.siblings.slice(), [], 'siblings cp should have calculated empty initially'); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref], - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], }, }, - included: [sibling1], - }); + }, + included: [sibling1], }); - run(() => { - let cpResult = get(obj, 'siblings').slice(); - assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); - obj.destroy(); - }); + const cpResult = obj.siblings.slice(); + assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); + obj.destroy(); } ); @@ -128,57 +122,53 @@ module('integration/records/relationship-changes - Relationship changes', functi '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(2); + assert.expect(3); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let Obj = EmberObject.extend({ + const Obj = EmberObject.extend({ person: null, firstSibling: alias('person.siblings.firstObject'), }); const obj = Obj.create(); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [], - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], }, }, - }); - set(obj, 'person', store.peekRecord('person', 'wat')); + }, }); + obj.person = store.peekRecord('person', 'wat'); + assert.strictEqual(obj.sibling, undefined, 'We have no first sibling initially'); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref], - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], }, }, - included: [sibling1], - }); + }, + included: [sibling1], }); - run(() => { - let cpResult = get(obj, 'firstSibling'); - assert.strictEqual(get(cpResult, 'id'), '1', 'siblings cp should have recalculated'); - obj.destroy(); - }); + const cpResult = obj.firstSibling; + assert.strictEqual(cpResult?.id, '1', 'siblings cp should have recalculated'); + obj.destroy(); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); } ); @@ -331,62 +321,53 @@ module('integration/records/relationship-changes - Relationship changes', functi assert.ok(observerCount >= 1, 'siblings observer should be triggered at least once'); }); - test('Calling push with relationship does not trigger observers if the relationship was not changed', function (assert) { + test('Calling push with relationship does not trigger observers if the relationship was not changed', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let person = null; + const store = this.owner.lookup('service:store'); let observerCount = 0; - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - relationships: { - siblings: { - data: [sibling1Ref], - }, + const person = store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [sibling1Ref], }, }, - included: [sibling1], - }); - person = store.peekRecord('person', 'wat'); + }, + included: [sibling1], }); const observerMethod = function () { observerCount++; }; - run(() => { - // prime the pump - person.siblings; - person.addObserver('siblings.[]', observerMethod); - }); + // prime the pump + person.siblings; + person.addObserver('siblings.[]', observerMethod); - run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: {}, - relationships: { - siblings: { - data: [sibling1Ref], - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], }, }, - included: [], - }); + }, + included: [], }); - run(() => { - assert.strictEqual(observerCount, 0, 'siblings observer should not be triggered'); - }); + await settled(); + assert.strictEqual(observerCount, 0, 'siblings observer should not be triggered'); person.removeObserver('siblings.[]', observerMethod); }); diff --git a/tests/main/tests/integration/records/reload-test.js b/tests/main/tests/integration/records/reload-test.js index 0282f3721ba..1e267eb9eeb 100644 --- a/tests/main/tests/integration/records/reload-test.js +++ b/tests/main/tests/integration/records/reload-test.js @@ -1,7 +1,6 @@ import { get } from '@ember/object'; import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -25,7 +24,7 @@ module('integration/reload - Reloading Records', function (hooks) { lastName; } - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register( 'serializer:application', @@ -40,7 +39,7 @@ module('integration/reload - Reloading Records', function (hooks) { test("When a single record is requested, the adapter's find method should be called unless it's loaded.", async function (assert) { let count = 0; - let reloadOptions = { + const reloadOptions = { adapterOptions: { makeSnazzy: true, }, @@ -56,7 +55,7 @@ module('integration/reload - Reloading Records', function (hooks) { findRecord(store, type, id, snapshot) { if (count === 0) { count++; - return resolve({ data: { id: id, type: 'person', attributes: { name: 'Tom Dale' } } }); + return Promise.resolve({ data: { id: id, type: 'person', attributes: { name: 'Tom Dale' } } }); } else if (count === 1) { assert.strictEqual( snapshot.adapterOptions, @@ -64,7 +63,7 @@ module('integration/reload - Reloading Records', function (hooks) { 'We passed adapterOptions via reload' ); count++; - return resolve({ + return Promise.resolve({ data: { id: id, type: 'person', attributes: { name: 'Braaaahm Dale' } }, }); } else { @@ -74,12 +73,12 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.strictEqual(get(person, 'name'), 'Tom Dale', 'The person is loaded with the right name'); assert.true(get(person, 'isLoaded'), 'The person is now loaded'); - let promise = person.reload(reloadOptions); + const promise = person.reload(reloadOptions); assert.true(get(person, 'isReloading'), 'The person is now reloading'); @@ -93,7 +92,7 @@ module('integration/reload - Reloading Records', function (hooks) { }); test('When a record is reloaded and fails, it can try again', async function (assert) { - let tom = store.push({ + const tom = store.push({ data: { type: 'person', id: '1', @@ -114,9 +113,9 @@ module('integration/reload - Reloading Records', function (hooks) { findRecord() { assert.true(tom.isReloading, 'Tom is reloading'); if (count++ === 0) { - return reject(); + return Promise.reject(); } else { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Thomas Dale' } }, }); } @@ -134,7 +133,7 @@ module('integration/reload - Reloading Records', function (hooks) { assert.true(tom.isError, 'Tom is now errored'); assert.false(tom.isReloading, 'Tom is no longer reloading'); - let person = await tom.reload(); + const person = await tom.reload(); assert.strictEqual(person, tom, 'The resolved value is the record'); assert.false(tom.isError, 'Tom is no longer errored'); @@ -165,7 +164,7 @@ module('integration/reload - Reloading Records', function (hooks) { findRecord(store, type, id, snapshot) { assert.ok(true, 'We should call findRecord'); - return resolve(getTomDale()); + return Promise.resolve(getTomDale()); }, }) ); @@ -179,7 +178,7 @@ module('integration/reload - Reloading Records', function (hooks) { store.push(getTomDale()); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); person.addObserver('isLoaded', isLoadedDidChange); assert.true(get(person, 'isLoaded'), 'The person is loaded'); @@ -208,7 +207,7 @@ module('integration/reload - Reloading Records', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:tag', Tag); - let tagsById = { 1: 'hipster', 2: 'hair' }; + const tagsById = { 1: 'hipster', 2: 'hair' }; this.owner.register( 'adapter:application', @@ -220,7 +219,7 @@ module('integration/reload - Reloading Records', function (hooks) { findRecord(store, type, id, snapshot) { switch (type.modelName) { case 'person': - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -236,16 +235,15 @@ module('integration/reload - Reloading Records', function (hooks) { }, }); case 'tag': - return resolve({ data: { id: id, type: 'tag', attributes: { name: tagsById[id] } } }); + return Promise.resolve({ data: { id: id, type: 'tag', attributes: { name: tagsById[id] } } }); } }, }) ); - let tom; let person = await store.findRecord('person', '1'); - tom = person; + const tom = person; assert.strictEqual(person.name, 'Tom', 'precond'); let tags = await person.tags; @@ -283,7 +281,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findRecord() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -296,7 +294,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -318,9 +316,9 @@ module('integration/reload - Reloading Records', function (hooks) { ], }); - let ownerRef = shen.belongsTo('owner'); - let owner = shen.owner; - let ownerViaRef = await ownerRef.reload(); + const ownerRef = shen.belongsTo('owner'); + const owner = shen.owner; + const ownerViaRef = await ownerRef.reload(); assert.strictEqual(owner, ownerViaRef, 'We received the same reference via reload'); }); @@ -340,7 +338,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findRecord() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -353,7 +351,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -366,9 +364,9 @@ module('integration/reload - Reloading Records', function (hooks) { }, }); - let ownerRef = shen.belongsTo('owner'); - let ownerViaRef = await ownerRef.reload(); - let owner = shen.owner; + const ownerRef = shen.belongsTo('owner'); + const ownerViaRef = await ownerRef.reload(); + const owner = shen.owner; assert.strictEqual(owner, ownerViaRef, 'We received the same reference via reload'); }); @@ -388,7 +386,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findRecord() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -401,7 +399,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -423,9 +421,9 @@ module('integration/reload - Reloading Records', function (hooks) { ], }); - let ownersRef = shen.hasMany('owners'); - let owners = shen.owners; - let ownersViaRef = await ownersRef.reload(); + const ownersRef = shen.hasMany('owners'); + const owners = shen.owners; + const ownersViaRef = await ownersRef.reload(); assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); @@ -445,7 +443,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findRecord() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -458,7 +456,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -471,9 +469,9 @@ module('integration/reload - Reloading Records', function (hooks) { }, }); - let ownersRef = shen.hasMany('owners'); - let ownersViaRef = await ownersRef.reload(); - let owners = shen.owners; + const ownersRef = shen.hasMany('owners'); + const ownersViaRef = await ownersRef.reload(); + const owners = shen.owners; assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); @@ -495,7 +493,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findBelongsTo() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -508,7 +506,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -533,9 +531,9 @@ module('integration/reload - Reloading Records', function (hooks) { ], }); - let ownerRef = shen.belongsTo('owner'); - let owner = shen.owner; - let ownerViaRef = await ownerRef.reload(); + const ownerRef = shen.belongsTo('owner'); + const owner = shen.owner; + const ownerViaRef = await ownerRef.reload(); assert.strictEqual(owner, ownerViaRef, 'We received the same reference via reload'); }); @@ -555,7 +553,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findBelongsTo() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -568,7 +566,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -584,9 +582,9 @@ module('integration/reload - Reloading Records', function (hooks) { }, }); - let ownerRef = shen.belongsTo('owner'); - let ownerViaRef = await ownerRef.reload(); - let owner = shen.owner; + const ownerRef = shen.belongsTo('owner'); + const ownerViaRef = await ownerRef.reload(); + const owner = shen.owner; assert.strictEqual(owner, ownerViaRef, 'We received the same reference via reload'); }); @@ -606,7 +604,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findHasMany() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: [ { type: 'person', @@ -621,7 +619,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -646,9 +644,9 @@ module('integration/reload - Reloading Records', function (hooks) { ], }); - let ownersRef = shen.hasMany('owners'); - let owners = shen.owners; - let ownersViaRef = await ownersRef.reload(); + const ownersRef = shen.hasMany('owners'); + const owners = shen.owners; + const ownersViaRef = await ownersRef.reload(); assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); @@ -668,7 +666,7 @@ module('integration/reload - Reloading Records', function (hooks) { JSONAPIAdapter.extend({ findHasMany() { assert.ok('We called findRecord'); - return resolve({ + return Promise.resolve({ data: [ { type: 'person', @@ -683,7 +681,7 @@ module('integration/reload - Reloading Records', function (hooks) { }) ); - let shen = store.push({ + const shen = store.push({ data: { type: 'pet', id: '1', @@ -699,9 +697,9 @@ module('integration/reload - Reloading Records', function (hooks) { }, }); - let ownersRef = shen.hasMany('owners'); - let ownersViaRef = await ownersRef.reload(); - let owners = shen.owners; + const ownersRef = shen.hasMany('owners'); + const ownersViaRef = await ownersRef.reload(); + const owners = shen.owners; assert.strictEqual(owners.at(0), ownersViaRef.at(0), 'We received the same reference via reload'); }); diff --git a/tests/main/tests/integration/records/rematerialize-test.js b/tests/main/tests/integration/records/rematerialize-test.js index db2c5cadf77..7b07502e41e 100644 --- a/tests/main/tests/integration/records/rematerialize-test.js +++ b/tests/main/tests/integration/records/rematerialize-test.js @@ -1,7 +1,3 @@ -/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "(adam|bob|dudu)" }]*/ - -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; @@ -9,7 +5,6 @@ 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 deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; module('integration/unload - Rematerializing Unloaded Records', function (hooks) { setupTest(hooks); @@ -20,69 +15,58 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) }); test('a sync belongs to relationship to an unloaded record can restore that record', function (assert) { - const Person = Model.extend({ - name: attr('string'), - cars: hasMany('car', { async: false, inverse: 'person' }), - toString: () => 'Person', - }); + class Person extends Model { + @attr('string') name; + @hasMany('car', { async: false, inverse: 'person' }) cars; + } - const Car = Model.extend({ - make: attr('string'), - model: attr('string'), - person: belongsTo('person', { async: false, inverse: 'cars' }), - toString: () => 'Car', - }); + class Car extends Model { + @attr('string') make; + @attr('string') model; + @belongsTo('person', { async: false, inverse: 'cars' }) person; + } this.owner.register('model:person', Person); this.owner.register('model:car', Car); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); // disable background reloading so we do not re-create the relationship. adapter.shouldBackgroundReloadRecord = () => false; - let adam = run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - relationships: { - cars: { - data: [{ type: 'car', id: '1' }], - }, + const adam = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Adam Sunderland', + }, + relationships: { + cars: { + data: [{ type: 'car', id: '1' }], }, }, - }); - - return store.peekRecord('person', 1); + }, }); - let bob = run(() => { - store.push({ - data: { - type: 'car', - id: '1', - attributes: { - make: 'Lotus', - model: 'Exige', - }, - relationships: { - person: { - data: { type: 'person', id: '1' }, - }, + const lotus = store.push({ + data: { + type: 'car', + id: '1', + attributes: { + make: 'Lotus', + model: 'Exige', + }, + relationships: { + person: { + data: { type: 'person', id: '1' }, }, }, - }); - - return store.peekRecord('car', 1); + }, }); - let person = store.peekRecord('person', 1); - assert.strictEqual(person.cars.length, 1, 'The inital length of cars is correct'); + assert.strictEqual(adam.cars.length, 1, 'The inital length of cars is correct'); assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( @@ -90,7 +74,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) 'The person identifier is loaded' ); - run(() => person.unloadRecord()); + adam.unloadRecord(); assert.strictEqual(store.peekRecord('person', '1'), null, 'The person is unloaded'); assert.false( @@ -98,26 +82,25 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) 'The person identifier is freed' ); - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - relationships: { - cars: { - data: [{ type: 'car', id: '1' }], - }, + const newAdam = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Adam Sunderland', + }, + relationships: { + cars: { + data: [{ type: 'car', id: '1' }], }, }, - }); + }, }); - let rematerializedPerson = bob.person; + const rematerializedPerson = lotus.person; assert.strictEqual(rematerializedPerson.id, '1'); assert.strictEqual(rematerializedPerson.name, 'Adam Sunderland'); + assert.strictEqual(rematerializedPerson, newAdam); // the person is rematerialized; the previous person is *not* re-used assert.notEqual(rematerializedPerson, adam, 'the person is rematerialized, not recycled'); }); @@ -140,8 +123,8 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) this.owner.register('model:person', Person); this.owner.register('model:boat', Boat); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); // disable background reloading so we do not re-create the relationship. adapter.shouldBackgroundReloadRecord = () => false; @@ -178,9 +161,9 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) let data; if (param === '1') { - data = deepCopy(BOAT_ONE); + data = structuredClone(BOAT_ONE); } else if (param === '2') { - data = deepCopy(BOAT_TWO); + data = structuredClone(BOAT_TWO); } else { throw new Error(`404: no such boat with id=${param}`); } @@ -190,7 +173,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) }; }; - let adam = store.push({ + const adam = store.push({ data: { type: 'person', id: '1', @@ -208,8 +191,8 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) }, }); - let [boaty] = store.push({ - data: [deepCopy(BOAT_ONE), deepCopy(BOAT_TWO)], + const [boaty] = store.push({ + data: [structuredClone(BOAT_ONE), structuredClone(BOAT_TWO)], }); // assert our initial cache state @@ -236,7 +219,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) // cause a rematerialization, this should also cause us to fetch boat '1' again boats = await adam.boats; - let rematerializedBoaty = boats.at(1); + const rematerializedBoaty = boats.at(1); assert.ok(!!rematerializedBoaty, 'We have a boat!'); assert.strictEqual(adam.boats.length, 2, 'boats.length correct after rematerialization'); diff --git a/tests/main/tests/integration/records/save-test.js b/tests/main/tests/integration/records/save-test.js index 1d6a28abb89..762ce8e5527 100644 --- a/tests/main/tests/integration/records/save-test.js +++ b/tests/main/tests/integration/records/save-test.js @@ -1,16 +1,16 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { defer } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import { InvalidError } from '@ember-data/adapter/error'; -import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@ember-data/deprecations'; 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); @@ -26,16 +26,16 @@ module('integration/records/save - Save Record', function (hooks) { }); test('Will resolve save on success', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); - const deferred = defer(); + const deferred = createDeferred(); adapter.createRecord = function (store, type, snapshot) { return deferred.promise; }; - let saved = post.save(); + const saved = post.save(); if (DEPRECATE_SAVE_PROMISE_ACCESS) { // `save` returns a PromiseObject which allows to call get on it @@ -43,7 +43,7 @@ module('integration/records/save - Save Record', function (hooks) { } deferred.resolve({ data: { id: '123', type: 'post' } }); - let model = await saved; + const model = await saved; assert.ok(true, 'save operation was resolved'); if (DEPRECATE_SAVE_PROMISE_ACCESS) { assert.strictEqual(saved.get('id'), '123', `.get('id') is '123' after save resolves`); @@ -69,19 +69,19 @@ module('integration/records/save - Save Record', function (hooks) { }); test('Will reject save on error', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { - let error = new InvalidError([{ title: 'not valid' }]); + const error = new InvalidError([{ title: 'not valid' }]); return Promise.reject(error); }; try { await post.save(); assert.ok(false, 'we should err'); - } catch (error) { + } catch { assert.ok(true, 'we errored during save'); } }); @@ -114,14 +114,14 @@ module('integration/records/save - Save Record', function (hooks) { }); test('Retry is allowed in a failure handler', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); var count = 0; adapter.createRecord = function (store, type, snapshot) { - let error = new InvalidError([{ title: 'not valid' }]); + const error = new InvalidError([{ title: 'not valid' }]); if (count++ === 0) { return Promise.reject(error); @@ -138,12 +138,12 @@ module('integration/records/save - Save Record', function (hooks) { assert.strictEqual(post.id, '123', 'The post ID made it through'); }); - test('Repeated failed saves keeps the record in uncommited state', async function (assert) { + test('Repeated failed saves keeps the record in uncommitted state', async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { return Promise.reject(); @@ -169,12 +169,12 @@ module('integration/records/save - Save Record', function (hooks) { test('Repeated failed saves with invalid error marks the record as invalid', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { - let error = new InvalidError([ + const error = new InvalidError([ { detail: 'is invalid', source: { pointer: 'data/attributes/title' }, @@ -201,12 +201,12 @@ module('integration/records/save - Save Record', function (hooks) { test('Repeated failed saves with invalid error without payload marks the record as invalid', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { - let error = new InvalidError(); + const error = new InvalidError(); return Promise.reject(error); }; @@ -227,9 +227,9 @@ module('integration/records/save - Save Record', function (hooks) { test('Will reject save on invalid', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { var error = new InvalidError([{ title: 'not valid' }]); @@ -271,9 +271,9 @@ module('integration/records/save - Save Record', function (hooks) { test('Will error when saving after unloading record via the store', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { return { @@ -296,9 +296,9 @@ module('integration/records/save - Save Record', function (hooks) { test('Will error when saving after unloading record', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let post = store.createRecord('post', { title: 'toto' }); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const post = store.createRecord('post', { title: 'toto' }); adapter.createRecord = function (store, type, snapshot) { return { diff --git a/tests/main/tests/integration/records/unload-test.js b/tests/main/tests/integration/records/unload-test.js index 7603021e117..6903bcf2d22 100644 --- a/tests/main/tests/integration/records/unload-test.js +++ b/tests/main/tests/integration/records/unload-test.js @@ -1,16 +1,10 @@ -/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "(adam|bob|dudu)" }]*/ - -import { get } from '@ember/object'; -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { all, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/deprecations'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import { recordIdentifierFor } from '@ember-data/store'; @@ -126,7 +120,7 @@ module('integration/unload - Unloading Records', function (hooks) { let store, adapter; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register(`model:person`, Person); owner.register(`model:car`, Car); @@ -185,75 +179,66 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(adam, null, 'we have no person'); }); - test('can unload all records for a given type', function (assert) { + test('can unload all records for a given type', async function (assert) { assert.expect(6); let car; - run(function () { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - }, - { - type: 'person', - id: '2', - attributes: { - name: 'Bob Bobson', - }, - }, - ], - }); - let adam = store.peekRecord('person', 1); - let bob = store.peekRecord('person', 2); - - car = store.push({ - data: { - type: 'car', + store.push({ + data: [ + { + type: 'person', id: '1', attributes: { - make: 'VW', - model: 'Beetle', + name: 'Adam Sunderland', }, - relationships: { - person: { - data: { type: 'person', id: '1' }, - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Bob Bobson', }, }, - }); - bob = store.peekRecord('car', 1); + ], + }); + + car = store.push({ + data: { + type: 'car', + id: '1', + attributes: { + make: 'VW', + model: 'Beetle', + }, + relationships: { + person: { + data: { type: 'person', id: '1' }, + }, + }, + }, }); assert.strictEqual(store.peekAll('person').length, 2, 'two person records loaded'); assert.strictEqual(store.peekAll('car').length, 1, 'one car record loaded'); - run(function () { - car.person; - store.unloadAll('person'); - }); + await car.person; + store.unloadAll('person'); assert.strictEqual(store.peekAll('person').length, 0); assert.strictEqual(store.peekAll('car').length, 1); - run(function () { - store.push({ - data: { - id: '1', - type: 'person', - attributes: { - name: 'Richard II', - }, + store.push({ + data: { + id: '1', + type: 'person', + attributes: { + name: 'Richard II', }, - }); + }, }); car = store.peekRecord('car', 1); - let person = car.person; + const person = car.person; assert.ok(!!car, 'We have a car'); assert.notOk(person, 'We dont have a person'); @@ -262,52 +247,45 @@ module('integration/unload - Unloading Records', function (hooks) { test('can unload all records', function (assert) { assert.expect(4); - run(function () { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - }, - { - type: 'person', - id: '2', - attributes: { - name: 'Bob Bobson', - }, - }, - ], - }); - let adam = store.peekRecord('person', 1); - let bob = store.peekRecord('person', 2); - - store.push({ - data: { - type: 'car', + store.push({ + data: [ + { + type: 'person', id: '1', attributes: { - make: 'VW', - model: 'Beetle', + name: 'Adam Sunderland', }, - relationships: { - person: { - data: { type: 'person', id: '1' }, - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Bob Bobson', }, }, - }); - bob = store.peekRecord('car', 1); + ], + }); + + store.push({ + data: { + type: 'car', + id: '1', + attributes: { + make: 'VW', + model: 'Beetle', + }, + relationships: { + person: { + data: { type: 'person', id: '1' }, + }, + }, + }, }); assert.strictEqual(store.peekAll('person').length, 2, 'two person records loaded'); assert.strictEqual(store.peekAll('car').length, 1, 'one car record loaded'); - run(function () { - store.unloadAll(); - }); + store.unloadAll(); assert.strictEqual(store.peekAll('person').length, 0); assert.strictEqual(store.peekAll('car').length, 0); @@ -642,35 +620,29 @@ module('integration/unload - Unloading Records', function (hooks) { }); test('unloading all records also updates record array from peekAll()', function (assert) { - run(function () { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Adam Sunderland', }, - { - type: 'person', - id: '2', - attributes: { - name: 'Bob Bobson', - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Bob Bobson', }, - ], - }); - let adam = store.peekRecord('person', 1); - let bob = store.peekRecord('person', 2); + }, + ], }); - let all = store.peekAll('person'); + const all = store.peekAll('person'); assert.strictEqual(all.length, 2); - run(function () { - store.unloadAll('person'); - }); + store.unloadAll('person'); assert.strictEqual(all.length, 0); }); @@ -715,14 +687,14 @@ module('integration/unload - Unloading Records', function (hooks) { }); const all2 = store.peekAll('person'); assert.strictEqual(all2.length, 1, 'after next push: record array has one item'); - // eslint-disable-next-line qunit/no-ok-equality + assert.true(all === all2, 'after next push: record array is the same'); }); test('unloadAll(type) does not leave stranded internalModels in relationships (rediscover via store.push)', async function (assert) { - assert.expect(13); + assert.expect(16); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -738,58 +710,57 @@ module('integration/unload - Unloading Records', function (hooks) { included: [makeBoatOneForPersonOne()], }); - let boat = store.peekRecord('boat', '1'); - let relationshipState = person.hasMany('boats').hasManyRelationship; + const boat = store.peekRecord('boat', '1'); + const relationshipState = person.hasMany('boats').hasManyRelationship; // ensure we loaded the people and boats assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); // ensure the relationship was established (we reach through the async proxy here) - let peopleBoats = await person.boats; - let boatPerson = await boat.person; + const peopleBoats = await person.boats; + const boatPerson = await boat.person; assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has a boat'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); + assert.strictEqual(peopleBoats.length, 1, 'Our person has a boat'); assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); - run(() => { - store.unloadAll('boat'); - }); + store.unloadAll('boat'); // ensure that our new state is correct assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should still be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should still be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 0, 'Our person thinks they have no boats'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); + assert.strictEqual(peopleBoats.length, 0, 'Our person thinks they have no boats'); - run(() => - store.push({ - data: makeBoatOneForPersonOne(), - }) - ); + store.push({ + data: makeBoatOneForPersonOne(), + }); store.peekRecord('boat', '1'); // ensure that our new state is correct assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should still be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should still be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has their boats'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); + assert.strictEqual(peopleBoats.length, 1, 'Our person has their boats'); }); test('unloadAll(type) does not leave stranded internalModels in relationships (rediscover via relationship reload)', async function (assert) { - assert.expect(15); + assert.expect(18); adapter.findRecord = (store, type, id) => { assert.strictEqual(type.modelName, 'boat', 'We refetch the boat'); assert.strictEqual(id, '1', 'We refetch the right boat'); - return resolve({ + return Promise.resolve({ data: makeBoatOneForPersonOne(), }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -805,18 +776,19 @@ module('integration/unload - Unloading Records', function (hooks) { included: [makeBoatOneForPersonOne()], }); - let boat = store.peekRecord('boat', '1'); - let relationshipState = person.hasMany('boats').hasManyRelationship; + const boat = store.peekRecord('boat', '1'); + const relationshipState = person.hasMany('boats').hasManyRelationship; // ensure we loaded the people and boats assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); - let peopleBoats = await person.boats; - let boatPerson = await boat.person; + const peopleBoats = await person.boats; + const boatPerson = await boat.person; assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should be 1'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); assert.strictEqual(peopleBoats.length, 1, 'Our person has a boat'); assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); @@ -826,7 +798,8 @@ module('integration/unload - Unloading Records', function (hooks) { // ensure that our new state is correct assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should still be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should still be 1'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); assert.strictEqual(peopleBoats.length, 0, 'Our person thinks they have no boats'); await person.boats; @@ -834,106 +807,102 @@ module('integration/unload - Unloading Records', function (hooks) { store.peekRecord('boat', '1'); assert.strictEqual(relationshipState.remoteState.length, 1, 'remoteMembers size should still be 1'); - assert.strictEqual(relationshipState.localMembers.size, 1, 'localMembers size should still be 1'); + assert.strictEqual(relationshipState.additions, null, 'additions should be empty'); + assert.strictEqual(relationshipState.removals, null, 'removals should be empty'); assert.strictEqual(peopleBoats.length, 1, 'Our person has their boats'); }); test('(regression) unloadRecord followed by push in the same run-loop', async function (assert) { - let person = run(() => - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Could be Anybody', - }, - relationships: { - boats: { - data: [{ type: 'boat', id: '1' }], - }, + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Could be Anybody', + }, + relationships: { + boats: { + data: [{ type: 'boat', id: '1' }], }, }, - included: [makeBoatOneForPersonOne()], - }) - ); + }, + included: [makeBoatOneForPersonOne()], + }); let boat = store.peekRecord('boat', '1'); - let relationshipState = person.hasMany('boats').hasManyRelationship; + const relationshipState = person.hasMany('boats').hasManyRelationship; // ensure we loaded the people and boats assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); - // ensure the relationship was established (we reach through the async proxy here) - let peopleBoats = run(() => person.boats.content); - let boatPerson = run(() => boat.person.content); + // ensure the relationship was established + const peopleBoats = await person.boats; + const boatPerson = await boat.person; assert.deepEqual(idsFromArr(relationshipState.remoteState), ['1'], 'remoteMembers size should be 1'); assert.deepEqual(idsFromArr(relationshipState.localState), ['1'], 'localMembers size should be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has a boat'); + assert.strictEqual(peopleBoats.length, 1, 'Our person has a boat'); assert.strictEqual(peopleBoats.at(0), boat, 'Our person has the right boat'); assert.strictEqual(boatPerson, person, 'Our boat has the right person'); - run(() => boat.unloadRecord()); + boat.unloadRecord(); // ensure that our new state is correct assert.deepEqual(idsFromArr(relationshipState.remoteState), ['1'], 'remoteMembers size should still be 1'); assert.deepEqual(idsFromArr(relationshipState.localState), ['1'], 'localMembers size should still be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 0, 'Our person thinks they have no boats'); + assert.strictEqual(peopleBoats.length, 0, 'Our person thinks they have no boats'); - run(() => - store.push({ - data: makeBoatOneForPersonOne(), - }) - ); + store.push({ + data: makeBoatOneForPersonOne(), + }); - let reloadedBoat = store.peekRecord('boat', '1'); + const reloadedBoat = store.peekRecord('boat', '1'); assert.deepEqual(idsFromArr(relationshipState.remoteState), ['1'], 'remoteMembers size should be 1'); assert.deepEqual(idsFromArr(relationshipState.localState), ['1'], 'localMembers size should be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person thas their boat'); + assert.strictEqual(peopleBoats.length, 1, 'Our person thas their boat'); // and now the kicker, run-loop fun! // here, we will dematerialize the record, but push it back into the store // all in the same run-loop! // effectively this tests that our destroySync is not stupid - run(() => { - reloadedBoat.unloadRecord(); - store.push({ - data: makeBoatOneForPersonOne(), - }); + await settled(); + reloadedBoat.unloadRecord(); + store.push({ + data: makeBoatOneForPersonOne(), }); + await settled(); boat = store.peekRecord('boat', '1'); assert.notStrictEqual(boat, null, 'we have a boat'); assert.deepEqual(idsFromArr(relationshipState.remoteState), ['1'], 'remoteMembers size should be 1'); assert.deepEqual(idsFromArr(relationshipState.localState), ['1'], 'localMembers size should be 1'); - assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person thas their boat'); + assert.strictEqual(peopleBoats.length, 1, 'Our person thas their boat'); // and the other way too! // and now the kicker, run-loop fun! // here, we will dematerialize the record, but push it back into the store // all in the same run-loop! // effectively this tests that our destroySync is not stupid - let newPerson; - run(() => { - person.unloadRecord(); - newPerson = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Could be Anybody', - }, - relationships: { - boats: { - data: [{ type: 'boat', id: '1' }], - }, + await settled(); + person.unloadRecord(); + const newPerson = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Could be Anybody', + }, + relationships: { + boats: { + data: [{ type: 'boat', id: '1' }], }, }, - }); + }, }); + await settled(); const relatedPerson = await boat.person; assert.notStrictEqual(relatedPerson, person, 'the original record is gone'); @@ -950,7 +919,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; // populate initial record - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -1027,7 +996,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; // populate initial record - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -1057,8 +1026,8 @@ module('integration/unload - Unloading Records', function (hooks) { ], }); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); @@ -1093,7 +1062,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; // populate initial record - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -1115,8 +1084,8 @@ module('integration/unload - Unloading Records', function (hooks) { ], }); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; const bike = store.peekRecord('bike', '1'); assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); @@ -1146,7 +1115,7 @@ module('integration/unload - Unloading Records', function (hooks) { // stub findRecord adapter.findRecord = () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -1168,8 +1137,8 @@ module('integration/unload - Unloading Records', function (hooks) { }, }); - let identifier = recordIdentifierFor(record); - const cache = DEPRECATE_V1_RECORD_DATA ? store._instanceCache.getResourceCache(identifier) : store.cache; + const identifier = recordIdentifierFor(record); + const cache = store.cache; assert.strictEqual(record.currentState.stateName, 'root.loaded.saved', 'We are loaded initially'); @@ -1187,7 +1156,7 @@ module('integration/unload - Unloading Records', function (hooks) { }); test('after unloading a record, the record can be saved again immediately', async function (assert) { - assert.expect(0); + assert.expect(1); const data = { data: { @@ -1199,7 +1168,7 @@ module('integration/unload - Unloading Records', function (hooks) { }, }; - adapter.createRecord = () => resolve(data); + adapter.createRecord = () => Promise.resolve(data); // add an initial record with id '1' to the store store.push(data); @@ -1209,6 +1178,7 @@ module('integration/unload - Unloading Records', function (hooks) { // create a new record that will again get id '1' from the backend await store.createRecord('person').save(); + assert.ok(true, 'it worked'); }); test('after unloading a record, pushing a new copy will setup relationships', function (assert) { @@ -1240,130 +1210,120 @@ module('integration/unload - Unloading Records', function (hooks) { }); } - run(() => { - store.push(personData); - }); + store.push(personData); - let adam = store.peekRecord('person', 1); + const adam = store.peekRecord('person', 1); assert.strictEqual(adam.cars.length, 0, 'cars hasMany starts off empty'); - run(() => pushCar()); + pushCar(); assert.strictEqual(adam.cars.length, 1, 'pushing car setups inverse relationship'); - run(() => adam.cars.at(0).unloadRecord()); + adam.cars.at(0).unloadRecord(); assert.strictEqual(adam.cars.length, 0, 'unloading car cleaned up hasMany'); - run(() => pushCar()); + pushCar(); assert.strictEqual(adam.cars.length, 1, 'pushing car again setups inverse relationship'); }); test('1:1 sync unload', function (assert) { - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - house: { - data: { - id: '2', - type: 'house', - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + house: { + data: { + id: '2', + type: 'house', }, }, }, - included: [ - { - id: '2', - type: 'house', - }, - ], - }) - ); + }, + included: [ + { + id: '2', + type: 'house', + }, + ], + }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); let house = store.peekRecord('house', 2); assert.strictEqual(person.house.id, '2', 'initially relationship established lhs'); assert.strictEqual(house.person.id, '1', 'initially relationship established rhs'); - run(() => house.unloadRecord()); + house.unloadRecord(); assert.strictEqual(person.house, null, 'unloading acts as a delete for sync relationships'); assert.strictEqual(store.peekRecord('house', '2'), null, 'unloaded record gone from store'); - house = run(() => - store.push({ - data: { - id: '2', - type: 'house', - }, - }) - ); + house = store.push({ + data: { + id: '2', + type: 'house', + }, + }); assert.notStrictEqual(store.peekRecord('house', '2'), null, 'unloaded record can be restored'); assert.strictEqual(person.house, null, 'restoring unloaded record does not restore relationship'); assert.strictEqual(house.person, null, 'restoring unloaded record does not restore relationship'); - run(() => - store.push({ - data: { - id: '2', - type: 'house', - relationships: { - person: { - data: { - id: '1', - type: 'person', - }, + store.push({ + data: { + id: '2', + type: 'house', + relationships: { + person: { + data: { + id: '1', + type: 'person', }, }, }, - }) - ); + }, + }); assert.strictEqual(person.house.id, '2', 'after unloading, relationship can be restored'); assert.strictEqual(house.person.id, '1', 'after unloading, relationship can be restored'); }); test('1:many sync unload 1 side', function (assert) { - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - cars: { - data: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + cars: { + data: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], }, }, - included: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }) - ); + }, + included: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], + }); let person = store.peekRecord('person', 1); - let car2 = store.peekRecord('car', 2); - let car3 = store.peekRecord('car', 3); - let cars = person.cars; + const car2 = store.peekRecord('car', 2); + const car3 = store.peekRecord('car', 3); + const cars = person.cars; assert.false(cars.isDestroyed, 'ManyArray not destroyed'); assert.deepEqual( @@ -1374,7 +1334,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(car2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(car3.person.id, '1', 'initially relationship established rhs'); - run(() => person.unloadRecord()); + person.unloadRecord(); assert.strictEqual(store.peekRecord('person', '1'), null, 'unloaded record gone from store'); @@ -1382,14 +1342,12 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(car3.person, null, 'unloading acts as delete for sync relationships'); assert.true(cars.isDestroyed, 'ManyArray destroyed'); - person = run(() => - store.push({ - data: { - id: '1', - type: 'person', - }, - }) - ); + person = store.push({ + data: { + id: '1', + type: 'person', + }, + }); assert.notStrictEqual(store.peekRecord('person', '1'), null, 'unloaded record can be restored'); assert.deepEqual( @@ -1400,28 +1358,26 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(car2.person, null, 'restoring unloaded record does not restore relationship'); assert.strictEqual(car3.person, null, 'restoring unloaded record does not restore relationship'); - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - cars: { - data: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + cars: { + data: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], }, }, - }) - ); + }, + }); assert.strictEqual(car2.person.id, '1', 'after unloading, relationship can be restored'); assert.strictEqual(car3.person.id, '1', 'after unloading, relationship can be restored'); @@ -1433,43 +1389,41 @@ module('integration/unload - Unloading Records', function (hooks) { }); test('1:many sync unload many side', function (assert) { - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - cars: { - data: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + cars: { + data: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], }, }, - included: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }) - ); + }, + included: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], + }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); let car2 = store.peekRecord('car', 2); - let car3 = store.peekRecord('car', 3); - let cars = person.cars; + const car3 = store.peekRecord('car', 3); + const cars = person.cars; assert.false(cars.isDestroyed, 'ManyArray not destroyed'); assert.deepEqual( @@ -1480,7 +1434,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(car2.person.id, '1', 'initially relationship established rhs'); assert.strictEqual(car3.person.id, '1', 'initially relationship established rhs'); - run(() => car2.unloadRecord()); + car2.unloadRecord(); assert.strictEqual(store.peekRecord('car', '2'), null, 'unloaded record gone from store'); @@ -1492,14 +1446,12 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.strictEqual(car3.person.id, '1', 'unloading one of a sync hasMany does not affect the rest'); - car2 = run(() => - store.push({ - data: { - id: '2', - type: 'car', - }, - }) - ); + car2 = store.push({ + data: { + id: '2', + type: 'car', + }, + }); assert.notStrictEqual(store.peekRecord('car', '2'), null, 'unloaded record can be restored'); assert.deepEqual( @@ -1509,29 +1461,26 @@ module('integration/unload - Unloading Records', function (hooks) { ); assert.strictEqual(car2.person, null, 'restoring unloaded record does not restore relationship'); - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - cars: { - data: [ - { - id: '2', - type: 'car', - }, - { - id: '3', - type: 'car', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + cars: { + data: [ + { + id: '2', + type: 'car', + }, + { + id: '3', + type: 'car', + }, + ], }, }, - }) - ); - + }, + }); assert.strictEqual(car2.person.id, '1', 'after unloading, relationship can be restored'); assert.deepEqual( person.cars.map((r) => r.id), @@ -1541,65 +1490,63 @@ module('integration/unload - Unloading Records', function (hooks) { }); test('many:many sync unload', function (assert) { - run(() => - store.push({ - data: [ - { - id: '1', - type: 'person', - relationships: { - groups: { - data: [ - { - id: '3', - type: 'group', - }, - { - id: '4', - type: 'group', - }, - ], - }, + store.push({ + data: [ + { + id: '1', + type: 'person', + relationships: { + groups: { + data: [ + { + id: '3', + type: 'group', + }, + { + id: '4', + type: 'group', + }, + ], }, }, - { - id: '2', - type: 'person', - relationships: { - groups: { - data: [ - { - id: '3', - type: 'group', - }, - { - id: '4', - type: 'group', - }, - ], - }, + }, + { + id: '2', + type: 'person', + relationships: { + groups: { + data: [ + { + id: '3', + type: 'group', + }, + { + id: '4', + type: 'group', + }, + ], }, }, - ], - included: [ - { - id: '3', - type: 'group', - }, - { - id: '4', - type: 'group', - }, - ], - }) - ); + }, + ], + included: [ + { + id: '3', + type: 'group', + }, + { + id: '4', + type: 'group', + }, + ], + }); - let person1 = store.peekRecord('person', 1); + const person1 = store.peekRecord('person', 1); let person2 = store.peekRecord('person', 2); - let group3 = store.peekRecord('group', 3); - let group4 = store.peekRecord('group', 4); - let p2groups = person2.groups; - let g3people = group3.people; + const group3 = store.peekRecord('group', 3); + const group4 = store.peekRecord('group', 4); + const p2groups = person2.groups; + const g3people = group3.people; assert.deepEqual( person1.groups.map((r) => r.id), @@ -1625,7 +1572,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.false(p2groups.isDestroyed, 'groups is not destroyed'); assert.false(g3people.isDestroyed, 'people is not destroyed'); - run(() => person2.unloadRecord()); + person2.unloadRecord(); assert.true(p2groups.isDestroyed, 'groups (unloaded side) is destroyed'); assert.false(g3people.isDestroyed, 'people (inverse) is not destroyed'); @@ -1648,14 +1595,12 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store.peekRecord('person', '2'), null, 'unloading removes record from store'); - person2 = run(() => - store.push({ - data: { - id: '2', - type: 'person', - }, - }) - ); + person2 = store.push({ + data: { + id: '2', + type: 'person', + }, + }); assert.notStrictEqual(store.peekRecord('person', '2'), null, 'unloaded record can be restored'); assert.deepEqual( @@ -1674,28 +1619,26 @@ module('integration/unload - Unloading Records', function (hooks) { 'restoring unloaded record does not restore relationship' ); - run(() => - store.push({ - data: { - id: '2', - type: 'person', - relationships: { - groups: { - data: [ - { - id: '3', - type: 'group', - }, - { - id: '4', - type: 'group', - }, - ], - }, + store.push({ + data: { + id: '2', + type: 'person', + relationships: { + groups: { + data: [ + { + id: '3', + type: 'group', + }, + { + id: '4', + type: 'group', + }, + ], }, }, - }) - ); + }, + }); assert.deepEqual( person2.groups.map((r) => r.id), @@ -1714,7 +1657,7 @@ module('integration/unload - Unloading Records', function (hooks) { ); }); - test('1:1 async unload', function (assert) { + test('1:1 async unload', async function (assert) { let findRecordCalls = 0; adapter.findRecord = (store, type, id) => { @@ -1730,48 +1673,44 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - mortgage: { - data: { - id: '2', - type: 'mortgage', - }, + const person = store.push({ + data: { + id: '1', + type: 'person', + relationships: { + mortgage: { + data: { + id: '2', + type: 'mortgage', }, }, }, - }) - ); + }, + }); let mortgage; - return run(() => - person.mortgage - .then((asyncRecord) => { - mortgage = asyncRecord; - return mortgage.person; - }) - .then(() => { - assert.strictEqual(mortgage.belongsTo('person').id(), '1', 'initially relationship established lhs'); - assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'initially relationship established rhs'); - - run(() => mortgage.unloadRecord()); - - assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'unload async is not treated as delete'); - - return person.mortgage; - }) - .then((refetchedMortgage) => { - assert.notEqual(mortgage, refetchedMortgage, 'the previously loaded record is not reused'); - - assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'unload async is not treated as delete'); - assert.strictEqual(refetchedMortgage.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - assert.strictEqual(findRecordCalls, 2); - }) - ); + await person.mortgage + .then((asyncRecord) => { + mortgage = asyncRecord; + return mortgage.person; + }) + .then(() => { + assert.strictEqual(mortgage.belongsTo('person').id(), '1', 'initially relationship established lhs'); + assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'initially relationship established rhs'); + + mortgage.unloadRecord(); + + assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'unload async is not treated as delete'); + + return person.mortgage; + }) + .then((refetchedMortgage) => { + assert.notEqual(mortgage, refetchedMortgage, 'the previously loaded record is not reused'); + + assert.strictEqual(person.belongsTo('mortgage').id(), '2', 'unload async is not treated as delete'); + assert.strictEqual(refetchedMortgage.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.strictEqual(findRecordCalls, 2); + }); }); test('1:many async unload 1 side', async function (assert) { @@ -1812,7 +1751,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = store.push({ + const person = store.push({ data: { id: '1', type: 'person', @@ -1832,11 +1771,10 @@ module('integration/unload - Unloading Records', function (hooks) { }, }, }); - let boats, boat2, boat3; const asyncRecords = await person.boats; - boats = asyncRecords; - [boat2, boat3] = boats.slice(); + const boats = asyncRecords; + const [boat2, boat3] = boats.slice(); await Promise.all([boat2, boat3].map((b) => b.person)); assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established lhs'); @@ -1845,7 +1783,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.false(boats.isDestroyed, 'ManyArray is not destroyed'); - run(() => person.unloadRecord()); + person.unloadRecord(); assert.true(boats.isDestroyed, 'ManyArray is destroyed when 1 side is unloaded'); assert.strictEqual(boat2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); @@ -1887,7 +1825,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = store.push({ + const person = store.push({ data: { id: '1', type: 'person', @@ -1910,6 +1848,7 @@ module('integration/unload - Unloading Records', function (hooks) { const boats = await person.boats; + // eslint-disable-next-line prefer-const let [boat2, boat3] = boats.slice(); await Promise.all([boat2.person, boat3.person]); @@ -1989,7 +1928,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let [person1, person2] = store.push({ + const [person1, person2] = store.push({ data: [ { id: '1', @@ -2033,7 +1972,7 @@ module('integration/unload - Unloading Records', function (hooks) { const person1Friends = await person1.friends; const [person3, person4] = person1Friends.slice(); - await all([person2.friends, person3.friends, person4.friends]); + await Promise.all([person2.friends, person3.friends, person4.friends]); assert.deepEqual(person1.hasMany('friends').ids(), ['3', '4'], 'initially relationship established lhs'); assert.deepEqual(person2.hasMany('friends').ids(), ['3', '4'], 'initially relationship established lhs'); @@ -2080,7 +2019,7 @@ module('integration/unload - Unloading Records', function (hooks) { }); test('1 sync : 1 async unload sync side', async function (assert) { - let person = store.push({ + const person = store.push({ data: { id: '1', type: 'person', @@ -2145,7 +2084,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(bookPerson?.id, '1', 'after unloading, relationship can be restored'); }); - test('1 sync : 1 async unload async side', function (assert) { + test('1 sync : 1 async unload async side', async function (assert) { let findRecordCalls = 0; adapter.findRecord = (store, type, id) => { @@ -2161,92 +2100,86 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - favoriteBook: { - data: { - id: '2', - type: 'book', - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + favoriteBook: { + data: { + id: '2', + type: 'book', }, }, }, - included: [ - { - id: '2', - type: 'book', - }, - ], - }) - ); + }, + included: [ + { + id: '2', + type: 'book', + }, + ], + }); - let person = store.peekRecord('person', 1); - let book = store.peekRecord('book', 2); + const person = store.peekRecord('person', 1); + const book = store.peekRecord('book', 2); - return run(() => - book.person - .then(() => { - assert.strictEqual(person.favoriteBook.id, '2', 'initially relationship established lhs'); - assert.strictEqual(book.belongsTo('person').id(), '1', 'initially relationship established rhs'); + await book.person + .then(() => { + assert.strictEqual(person.favoriteBook.id, '2', 'initially relationship established lhs'); + assert.strictEqual(book.belongsTo('person').id(), '1', 'initially relationship established rhs'); - run(() => person.unloadRecord()); + person.unloadRecord(); - assert.strictEqual(book.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.strictEqual(book.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - return book.person; - }) - .then((refetchedPerson) => { - assert.notEqual(person, refetchedPerson, 'the previously loaded record is not reused'); + return book.person; + }) + .then((refetchedPerson) => { + assert.notEqual(person, refetchedPerson, 'the previously loaded record is not reused'); - assert.strictEqual(book.belongsTo('person').id(), '1', 'unload async is not treated as delete'); - assert.strictEqual(refetchedPerson.favoriteBook.id, '2', 'unload async is not treated as delete'); - assert.strictEqual(findRecordCalls, 1); - }) - ); + assert.strictEqual(book.belongsTo('person').id(), '1', 'unload async is not treated as delete'); + assert.strictEqual(refetchedPerson.favoriteBook.id, '2', 'unload async is not treated as delete'); + assert.strictEqual(findRecordCalls, 1); + }); }); test('1 async : many sync unload sync side', function (assert) { - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - favoriteSpoons: { - data: [ - { - id: '2', - type: 'spoon', - }, - { - id: '3', - type: 'spoon', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + favoriteSpoons: { + data: [ + { + id: '2', + type: 'spoon', + }, + { + id: '3', + type: 'spoon', + }, + ], }, }, - included: [ - { - id: '2', - type: 'spoon', - }, - { - id: '3', - type: 'spoon', - }, - ], - }) - ); + }, + included: [ + { + id: '2', + type: 'spoon', + }, + { + id: '3', + type: 'spoon', + }, + ], + }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); let spoon2 = store.peekRecord('spoon', 2); - let spoon3 = store.peekRecord('spoon', 3); - let spoons = person.favoriteSpoons; + const spoon3 = store.peekRecord('spoon', 3); + const spoons = person.favoriteSpoons; assert.false(spoons.isDestroyed, 'ManyArray not destroyed'); assert.deepEqual( @@ -2257,7 +2190,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'initially relationship established rhs'); assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'initially relationship established rhs'); - run(() => spoon2.unloadRecord()); + spoon2.unloadRecord(); assert.strictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record gone from store'); @@ -2273,14 +2206,12 @@ module('integration/unload - Unloading Records', function (hooks) { 'unloading one of a sync hasMany does not affect the rest' ); - spoon2 = run(() => - store.push({ - data: { - id: '2', - type: 'spoon', - }, - }) - ); + spoon2 = store.push({ + data: { + id: '2', + type: 'spoon', + }, + }); assert.notStrictEqual(store.peekRecord('spoon', '2'), null, 'unloaded record can be restored'); assert.deepEqual( @@ -2294,28 +2225,26 @@ module('integration/unload - Unloading Records', function (hooks) { 'restoring unloaded record does not restore relationship' ); - run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - favoriteSpoons: { - data: [ - { - id: '2', - type: 'spoon', - }, - { - id: '3', - type: 'spoon', - }, - ], - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + favoriteSpoons: { + data: [ + { + id: '2', + type: 'spoon', + }, + { + id: '3', + type: 'spoon', + }, + ], }, }, - }) - ); + }, + }); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'after unloading, relationship can be restored'); assert.deepEqual( @@ -2343,7 +2272,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = store.push({ + const person = store.push({ data: { id: '1', type: 'person', @@ -2373,9 +2302,9 @@ module('integration/unload - Unloading Records', function (hooks) { }, ], }); - let spoon2 = store.peekRecord('spoon', '2'); - let spoon3 = store.peekRecord('spoon', '3'); - let spoons = person.favoriteSpoons; + const spoon2 = store.peekRecord('spoon', '2'); + const spoon3 = store.peekRecord('spoon', '3'); + const spoons = person.favoriteSpoons; assert.deepEqual( person.favoriteSpoons.map((r) => r.id), @@ -2433,7 +2362,7 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = store.push({ + const person = store.push({ data: { id: '1', type: 'person', @@ -2540,11 +2469,10 @@ module('integration/unload - Unloading Records', function (hooks) { }, }, }); - let shows, show2, show3; const asyncRecords = await person.favoriteShows; - shows = asyncRecords; - [show2, show3] = shows.slice(); + const shows = asyncRecords; + const [show2, show3] = shows.slice(); assert.deepEqual(person.hasMany('favoriteShows').ids(), ['2', '3'], 'initially relationship established lhs'); assert.strictEqual(show2.person.id, '1', 'initially relationship established rhs'); @@ -2615,7 +2543,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(findManyCalls, 1, 'findMany calls as expected'); }); - test('unload invalidates link promises', function (assert) { + test('unload invalidates link promises', async function (assert) { let isUnloaded = false; adapter.coalesceFindRequests = false; @@ -2627,7 +2555,7 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(snapshot.modelName, 'person', 'findHasMany(_, snapshot) is correct'); assert.strictEqual(link, 'boats', 'findHasMany(_, _, link) is correct'); - let relationships = { + const relationships = { person: { data: { type: 'person', @@ -2636,7 +2564,7 @@ module('integration/unload - Unloading Records', function (hooks) { }, }; - let data = [ + const data = [ { id: '3', type: 'boat', @@ -2657,57 +2585,48 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - let person = run(() => - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - boats: { - links: { related: 'boats' }, - }, + const person = store.push({ + data: { + id: '1', + type: 'person', + relationships: { + boats: { + links: { related: 'boats' }, }, }, - }) - ); + }, + }); let boats, boat2, boat3; - return run(() => - person.boats - .then((asyncRecords) => { - boats = asyncRecords; - [boat2, boat3] = boats.slice(); - }) - .then(() => { - assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established rhs'); - assert.strictEqual(boat2.belongsTo('person').id(), '1', 'initially relationship established rhs'); - assert.strictEqual(boat3.belongsTo('person').id(), '1', 'initially relationship established rhs'); - - isUnloaded = true; - run(() => { - boat2.unloadRecord(); - person.boats; - }); - - assert.deepEqual( - boats.map((r) => r.id), - ['3'], - 'unloaded boat is removed from ManyArray' - ); - }) - .then(() => { - return run(() => person.boats); - }) - .then((newBoats) => { - assert.strictEqual(newBoats.length, 1, 'new ManyArray has only 1 boat after unload'); - }) - ); + await person.boats + .then((asyncRecords) => { + boats = asyncRecords; + [boat2, boat3] = boats.slice(); + }) + .then(() => { + assert.deepEqual(person.hasMany('boats').ids(), ['2', '3'], 'initially relationship established rhs'); + assert.strictEqual(boat2.belongsTo('person').id(), '1', 'initially relationship established rhs'); + assert.strictEqual(boat3.belongsTo('person').id(), '1', 'initially relationship established rhs'); + + isUnloaded = true; + boat2.unloadRecord(); + return person.boats; + }) + .then((newBoats) => { + assert.deepEqual( + boats.map((r) => r.id), + ['3'], + 'unloaded boat is removed from ManyArray' + ); + assert.strictEqual(newBoats.length, 1, 'new ManyArray has only 1 boat after unload'); + }); }); - test('fetching records cancels unloading', function (assert) { + test('fetching records cancels unloading', async function (assert) { adapter.findRecord = (store, type, id) => { assert.strictEqual(type, Person, 'findRecord(_, type) is correct'); assert.deepEqual(id, '1', 'findRecord(_, _, id) is correct'); + assert.step('findRecord'); return { data: { @@ -2717,16 +2636,20 @@ module('integration/unload - Unloading Records', function (hooks) { }; }; - run(() => - store.push({ - data: { - id: '1', - type: 'person', - }, - }) - ); + store.push({ + data: { + id: '1', + type: 'person', + }, + }); - return run(() => store.findRecord('person', 1, { backgroundReload: true }).then((person) => person.unloadRecord())); + const person = await store.findRecord('person', '1', { backgroundReload: true }); + person.unloadRecord(); + assert.step('unloadRecord'); + await settled(); + assert.verifySteps(['unloadRecord', 'findRecord'], 'steps are correct'); + assert.notStrictEqual(store.peekRecord('person', '1'), null, 'record is still in the store'); + assert.true(person.isDestroyed, 'original record is destroyed'); }); test('edit then unloadAll removes all records (async) (emberjs/data#8863)', async function (assert) { diff --git a/tests/main/tests/integration/references/autotracking-test.js b/tests/main/tests/integration/references/autotracking-test.js index 7c240426e52..9e95ab31add 100644 --- a/tests/main/tests/integration/references/autotracking-test.js +++ b/tests/main/tests/integration/references/autotracking-test.js @@ -1,14 +1,14 @@ import EmberObject from '@ember/object'; import { getRootElement, render, settled } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; -import { DEBUG } from '@ember-data/env'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; +import { DEBUG } from '@warp-drive/build-config/env'; module('integration/references/autotracking', function (hooks) { setupRenderingTest(hooks); @@ -312,7 +312,7 @@ module('integration/references/autotracking', function (hooks) { } catch (e) { assert.strictEqual( e.message, - "Assertion Failed: Expected the ID received for the primary 'user' resource being saved to match the current id '6' but received '7'.", + "Expected the ID received for the primary 'user' resource being saved to match the current id '6' but received '7'.", 'threw error' ); } diff --git a/tests/main/tests/integration/references/belongs-to-test.js b/tests/main/tests/integration/references/belongs-to-test.js index 7af30997cfe..bed38c7385c 100644 --- a/tests/main/tests/integration/references/belongs-to-test.js +++ b/tests/main/tests/integration/references/belongs-to-test.js @@ -1,48 +1,48 @@ import { get } from '@ember/object'; import { module, test } from 'qunit'; -import { defer, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; 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'; -module('integration/references/belongs-to', function (hooks) { - setupTest(hooks); +class Family extends Model { + @hasMany('person', { async: true, inverse: 'family' }) persons; + @attr name; +} - hooks.beforeEach(function () { - const Family = Model.extend({ - persons: hasMany('person', { async: true, inverse: 'family' }), - name: attr(), - }); +class Team extends Model { + @hasMany('person', { async: true, inverse: 'team' }) persons; + @attr name; +} - const Team = Model.extend({ - persons: hasMany('person', { async: true, inverse: 'team' }), - name: attr(), - }); +class Person extends Model { + @belongsTo('family', { async: true, inverse: 'persons' }) family; + @belongsTo('team', { async: false, inverse: 'persons' }) team; +} - const Person = Model.extend({ - family: belongsTo('family', { async: true, inverse: 'persons' }), - team: belongsTo('team', { async: false, inverse: 'persons' }), - }); +module('integration/references/belongs-to', function (hooks) { + setupTest(hooks); + hooks.beforeEach(function () { this.owner.register('model:family', Family); this.owner.register('model:team', Team); this.owner.register('model:person', Person); - this.owner.register('adapter:application', JSONAPIAdapter.extend()); - this.owner.register('serializer:application', class extends JSONAPISerializer {}); + this.owner.register('adapter:application', JSONAPIAdapter); + this.owner.register('serializer:application', JSONAPISerializer); }); testInDebug("record#belongsTo asserts when specified relationship doesn't exist", function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -51,15 +51,15 @@ module('integration/references/belongs-to', function (hooks) { assert.expectAssertion(function () { person.belongsTo('unknown-relationship'); - }, 'Expected to find a relationship definition for person.unknown-relationship but none was found'); + }, "Expected a relationship schema for 'person.unknown-relationship', but no relationship schema was found."); }); testInDebug( "record#belongsTo asserts when the type of the specified relationship isn't the requested one", function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let family = store.push({ + const family = store.push({ data: { type: 'family', id: '1', @@ -73,9 +73,9 @@ module('integration/references/belongs-to', function (hooks) { ); test('record#belongsTo', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -87,7 +87,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.remoteType(), 'id'); assert.strictEqual(familyReference.type, 'family'); @@ -95,9 +95,9 @@ module('integration/references/belongs-to', function (hooks) { }); test('record#belongsTo for a linked reference', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -109,7 +109,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.remoteType(), 'link'); assert.strictEqual(familyReference.type, 'family'); @@ -117,9 +117,9 @@ module('integration/references/belongs-to', function (hooks) { }); test('BelongsToReference#meta() returns the most recent meta for the relationship', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -136,29 +136,159 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.deepEqual(familyReference.meta(), { foo: true }); }); - test('push(object)', async function (assert) { - let store = this.owner.lookup('service:store'); - let Family = store.modelFor('family'); + test('push(object) works with resources', async function (assert) { + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + + const data = { + data: { + type: 'family', + id: '1', + attributes: { + name: 'Coreleone', + }, + }, + }; + + const record = await familyReference.push(data); + assert.true(record instanceof Family, 'push resolves with the referenced record'); + assert.strictEqual(record.name, 'Coreleone', 'name is set'); + }); + + test('push(object) works with resource identifiers (skipLoad: false)', async function (assert) { + const store = this.owner.lookup('service:store'); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + included: [ + { + type: 'family', + id: '2', + attributes: { + name: 'Don Coreleone', + }, + }, + ], + }); + + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.id(), '1', 'id is correct'); + + const record = await familyReference.push({ + data: { + type: 'family', + id: '2', + }, + }); + assert.strictEqual(familyReference.id(), '2', 'id is correct'); + assert.strictEqual(record.name, 'Don Coreleone', 'name is correct'); + }); + + test('push(object) works with resource identifiers (skipLoad: true)', async function (assert) { + const store = this.owner.lookup('service:store'); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.id(), '1', 'id is correct'); + + await familyReference.push( + { + data: { + type: 'family', + id: '2', + }, + }, + true + ); + assert.strictEqual(familyReference.id(), '2', 'id is correct'); + }); + + test('push(object) works with null data', async function (assert) { + const store = this.owner.lookup('service:store'); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.id(), '1', 'id is correct'); + + await familyReference.push({ + data: null, + }); + assert.strictEqual(familyReference.id(), null, 'id is correct'); + }); + + test('push(object) works with links', async function (assert) { + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', relationships: { family: { + links: { related: '/person/1/families' }, data: { type: 'family', id: '1' }, }, }, }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.remoteType(), 'link', 'remoteType is link'); + assert.strictEqual(familyReference.link(), '/person/1/families', 'initial link is correct'); - let data = { + const data = { + links: { + related: '/person/1/families?page=1', + }, data: { type: 'family', id: '1', @@ -169,21 +299,129 @@ module('integration/references/belongs-to', function (hooks) { }; const record = await familyReference.push(data); - assert.ok(Family.detectInstance(record), 'push resolves with the referenced record'); - assert.strictEqual(get(record, 'name'), 'Coreleone', 'name is set'); + assert.true(record instanceof Family, 'push resolves with the referenced record'); + assert.strictEqual(record.name, 'Coreleone', 'name is set'); + assert.strictEqual(familyReference.link(), '/person/1/families?page=1', 'link is updated'); + }); + + test('push(object) works with links even when data is not present', async function (assert) { + const store = this.owner.lookup('service:store'); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + links: { related: '/person/1/families' }, + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.remoteType(), 'link', 'remoteType is link'); + assert.strictEqual(familyReference.link(), '/person/1/families', 'initial link is correct'); + assert.strictEqual(familyReference.id(), '1', 'id is correct'); + + const data = { + links: { + related: '/person/1/families?page=1', + }, + }; + + await familyReference.push(data, true); + assert.strictEqual(familyReference.id(), '1', 'id is still correct'); + assert.strictEqual(familyReference.link(), '/person/1/families?page=1', 'link is updated'); + }); + + test('push(object) works with meta', async function (assert) { + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); + const timestamp1 = Date.now(); + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + meta: { + createdAt: timestamp1, + }, + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + assert.deepEqual(familyReference.meta(), { createdAt: timestamp1 }, 'initial meta is correct'); + + const timestamp2 = Date.now() + 1; + const data = { + meta: { + updatedAt: timestamp2, + }, + data: { + type: 'family', + id: '1', + attributes: { + name: 'Coreleone', + }, + }, + }; + + const record = await familyReference.push(data); + assert.true(record instanceof Family, 'push resolves with the referenced record'); + assert.strictEqual(record.name, 'Coreleone', 'name is set'); + assert.deepEqual(familyReference.meta(), { updatedAt: timestamp2 }, 'meta is updated'); + }); + + test('push(object) works with meta even when data is not present', async function (assert) { + const store = this.owner.lookup('service:store'); + const timestamp1 = Date.now(); + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + meta: { + createdAt: timestamp1, + }, + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + + const familyReference = person.belongsTo('family'); + assert.strictEqual(familyReference.id(), '1', 'id is correct'); + assert.deepEqual(familyReference.meta(), { createdAt: timestamp1 }, 'initial meta is correct'); + + const timestamp2 = Date.now() + 1; + const data = { + meta: { + updatedAt: timestamp2, + }, + }; + + await familyReference.push(data, true); + assert.strictEqual(familyReference.id(), '1', 'id is still correct'); + 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) { - let store = this.owner.lookup('service:store'); - let Family = store.modelFor('family'); + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); - let push; - let deferred = defer(); + const deferred = createDeferred(); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -194,8 +432,8 @@ module('integration/references/belongs-to', function (hooks) { }, }, }); - let familyReference = person.belongsTo('family'); - push = familyReference.push(deferred.promise); + const familyReference = person.belongsTo('family'); + const push = familyReference.push(deferred.promise); assert.ok(push.then, 'BelongsToReference.push returns a promise'); @@ -229,9 +467,9 @@ module('integration/references/belongs-to', function (hooks) { this.owner.register('model:family', Family); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -242,21 +480,21 @@ module('integration/references/belongs-to', function (hooks) { }, }, }); - let anotherPerson = { + const anotherPerson = { data: { type: 'person', id: '2', }, }; - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); await assert.expectAssertion( async function () { await familyReference.push(anotherPerson); }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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'. 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'`." ); }); @@ -277,32 +515,38 @@ module('integration/references/belongs-to', function (hooks) { this.owner.register('model:family', Family); this.owner.register('model:person', Person); this.owner.register('model:mafia-family', MafiaFamily); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', + attributes: { + name: 'Vito', + }, }, }); - let mafiaFamily = { + const mafiaFamily = { data: { type: 'mafia-family', id: '1', + attributes: { + name: 'Don', + }, }, }; - let familyReference = person.belongsTo('family'); - let family = await familyReference.push(mafiaFamily); + const familyReference = person.belongsTo('family'); + const family = await familyReference.push(mafiaFamily); const record = store.peekRecord('mafia-family', '1'); - assert.strictEqual(family, record); + assert.strictEqual(family, record, 'we get back the correct record'); }); test('value() is null when reference is not yet loaded', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -314,14 +558,14 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.value(), null); }); test('value() returns the referenced record when loaded', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -332,20 +576,20 @@ module('integration/references/belongs-to', function (hooks) { }, }, }); - let family = store.push({ + const family = store.push({ data: { type: 'family', id: '1', }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.value(), family); }); test('value() returns the referenced record when loaded even if links are present', function (assert) { - let store = this.owner.lookup('service:store'); - let person = store.push({ + const store = this.owner.lookup('service:store'); + const person = store.push({ data: { type: 'person', id: '1', @@ -356,7 +600,7 @@ module('integration/references/belongs-to', function (hooks) { }, }, }); - let family = store.push({ + const family = store.push({ data: { type: 'family', id: '1', @@ -375,14 +619,14 @@ module('integration/references/belongs-to', function (hooks) { }); test('load() fetches the record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'family', @@ -391,7 +635,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -403,7 +647,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); const record = await familyReference.load({ adapterOptions }); @@ -411,14 +655,14 @@ module('integration/references/belongs-to', function (hooks) { }); test('load() fetches the record (sync belongsTo)', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; adapter.findRecord = function (store, type, id, snapshot) { assert.deepEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'team', @@ -427,7 +671,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -439,15 +683,15 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let teamReference = person.belongsTo('team'); + const teamReference = person.belongsTo('team'); const record = await teamReference.load({ adapterOptions }); assert.strictEqual(record.name, 'Tomsters'); }); test('load() fetches link when remoteType is link', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -455,7 +699,7 @@ module('integration/references/belongs-to', function (hooks) { assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); assert.strictEqual(link, '/families/1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'family', @@ -464,7 +708,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -476,7 +720,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); assert.strictEqual(familyReference.remoteType(), 'link'); await familyReference.load({ adapterOptions }).then(function (record) { @@ -485,8 +729,8 @@ module('integration/references/belongs-to', function (hooks) { }); test('meta can be retrieved, even if the fetched data is null', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -494,7 +738,7 @@ module('integration/references/belongs-to', function (hooks) { assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); assert.strictEqual(link, '/families/1', 'link was passed correctly'); - return resolve({ + return Promise.resolve({ data: null, meta: { it: 'works' }, }); @@ -522,8 +766,8 @@ module('integration/references/belongs-to', function (hooks) { }); test('reload() - loads the record when not yet loaded', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -534,7 +778,7 @@ module('integration/references/belongs-to', function (hooks) { count++; assert.strictEqual(count, 1); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'family', @@ -543,7 +787,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -555,81 +799,16 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); await familyReference.reload({ adapterOptions }).then(function (record) { assert.strictEqual(get(record, 'name'), 'Coreleone'); }); }); - test('reload() - loads the record when not yet loaded (sync)', async function (assert) { - class Familia extends Model { - @attr name; - } - - class Persona extends Model { - @belongsTo('familia', { async: false, inverse: null }) family; - @attr name; - } - - this.owner.register('model:familia', Familia); - this.owner.register('model:persona', Persona); - this.owner.register('adapter:application', class extends JSONAPIAdapter {}); - this.owner.register( - 'serializer:application', - class extends JSONAPISerializer { - normalizeResponse(_store, _schema, payload) { - return payload; - } - } - ); - + test('reload() - reloads the record when already loaded', async function (assert) { const store = this.owner.lookup('service:store'); const adapter = store.adapterFor('application'); - const adapterOptions = { thing: 'one' }; - - adapter.findRecord = function (store, type, id, snapshot) { - assert.step('findRecord'); - assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); - - return Promise.resolve({ - data: { - id: '1', - type: 'familia', - attributes: { name: 'Coreleone' }, - }, - }); - }; - - let person = store.push({ - data: { - type: 'persona', - id: '1', - attributes: { - name: 'Vito', - }, - relationships: { - family: { - data: { type: 'familia', id: '1' }, - }, - }, - }, - }); - - let familyReference = person.belongsTo('family'); - - await familyReference.reload({ adapterOptions }).then(function (record) { - assert.strictEqual(get(record, 'name'), 'Coreleone'); - }); - await familyReference.reload({ adapterOptions }).then(function (record) { - assert.strictEqual(get(record, 'name'), 'Coreleone'); - }); - assert.verifySteps(['findRecord', 'findRecord']); - }); - - test('reload() - reloads the record when already loaded', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -640,7 +819,7 @@ module('integration/references/belongs-to', function (hooks) { count++; assert.strictEqual(count, 1); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'family', @@ -649,7 +828,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -667,7 +846,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); await familyReference.reload({ adapterOptions }).then(function (record) { assert.strictEqual(get(record, 'name'), 'Coreleone'); @@ -675,8 +854,8 @@ module('integration/references/belongs-to', function (hooks) { }); test('reload() - uses link to reload record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -685,7 +864,7 @@ module('integration/references/belongs-to', function (hooks) { assert.strictEqual(link, '/families/1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'family', @@ -694,7 +873,7 @@ module('integration/references/belongs-to', function (hooks) { }); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -706,7 +885,7 @@ module('integration/references/belongs-to', function (hooks) { }, }); - let familyReference = person.belongsTo('family'); + const familyReference = person.belongsTo('family'); await familyReference.reload({ adapterOptions }).then(function (record) { assert.strictEqual(get(record, 'name'), 'Coreleone'); diff --git a/tests/main/tests/integration/references/has-many-test.js b/tests/main/tests/integration/references/has-many-test.js index 5fe0b513991..eb1b8561e9b 100755 --- a/tests/main/tests/integration/references/has-many-test.js +++ b/tests/main/tests/integration/references/has-many-test.js @@ -1,48 +1,45 @@ -import { get } from '@ember/object'; -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; -import { defer, resolve } from 'rsvp'; import { setupRenderingTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; 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'; -module('integration/references/has-many', function (hooks) { - setupRenderingTest(hooks); +class Family extends Model { + @hasMany('person', { async: true, inverse: 'family' }) persons; +} - hooks.beforeEach(function () { - const Family = Model.extend({ - persons: hasMany('person', { async: true, inverse: 'family' }), - }); +class Person extends Model { + @attr name; + @belongsTo('family', { async: true, inverse: 'persons' }) family; + @hasMany('pet', { async: true, inverse: null }) pets; +} - const Person = Model.extend({ - name: attr(), - family: belongsTo('family', { async: true, inverse: 'persons' }), - pets: hasMany('pet', { async: true, inverse: null }), - }); +class Pet extends Model { + @attr name; +} - const Pet = Model.extend({ - name: attr(), - }); +module('integration/references/has-many', function (hooks) { + setupRenderingTest(hooks); + hooks.beforeEach(function () { this.owner.register('model:family', Family); this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - this.owner.register('adapter:application', Adapter.extend()); - this.owner.register('serializer:application', class extends JSONAPISerializer {}); + this.owner.register('adapter:application', Adapter); + this.owner.register('serializer:application', JSONAPISerializer); }); testInDebug("record#hasMany asserts when specified relationship doesn't exist", function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const family = store.push({ data: { @@ -53,13 +50,13 @@ module('integration/references/has-many', function (hooks) { assert.expectAssertion(function () { family.hasMany('unknown-relationship'); - }, 'Expected to find a relationship definition for family.unknown-relationship but none was found'); + }, "Expected a relationship schema for 'family.unknown-relationship', but no relationship schema was found."); }); testInDebug( "record#hasMany asserts when the type of the specified relationship isn't the requested one", function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const person = store.push({ data: { @@ -75,7 +72,7 @@ module('integration/references/has-many', function (hooks) { ); test('record#hasMany', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const family = store.push({ data: { @@ -99,8 +96,20 @@ module('integration/references/has-many', function (hooks) { assert.deepEqual(personsReference.ids(), ['1', '2']); }); + test('ref.ids() updates when using createRecord', async function (assert) { + const store = this.owner.lookup('service:store'); + + const family = store.createRecord('family'); + const person1 = store.createRecord('person', {}); + assert.strictEqual(family.hasMany('persons').ids().length, 0); + + family.persons = [person1]; + + assert.strictEqual(family.hasMany('persons').ids().length, 1); + }); + test('record#hasMany for linked references', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const family = store.push({ data: { @@ -122,7 +131,7 @@ module('integration/references/has-many', function (hooks) { }); test('HasManyReference#meta() returns the most recent meta for the relationship', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const family = store.push({ data: { @@ -144,17 +153,17 @@ module('integration/references/has-many', function (hooks) { }); test('HasManyReference#value() does not create accidental autotracking errors', async function (assert) { - let store = this.owner.lookup('service:store'); - let family = store.push({ + const store = this.owner.lookup('service:store'); + const family = store.push({ data: { type: 'family', id: '1', }, }); - let personsReference = family.hasMany('persons'); + const personsReference = family.hasMany('persons'); let renderedValue; - let context = await createTrackingContext(this.owner, { + const context = await createTrackingContext({ get value() { renderedValue = personsReference.value(); return renderedValue; @@ -181,7 +190,7 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(renderedValue, null, 'We have no value yet, we are still not loaded'); - let person1 = store.push({ + const person1 = store.push({ data: { type: 'person', id: '1', @@ -232,7 +241,7 @@ module('integration/references/has-many', function (hooks) { await context.render(); - let person2 = store.peekRecord('person', '2'); + const person2 = store.peekRecord('person', '2'); assert.notStrictEqual(person2, null, 'we have a person'); assert.strictEqual(renderedValue.length, 2, 'We have two values'); assert.strictEqual(renderedValue.at(0), person1, 'We have the right value[0]'); @@ -258,12 +267,12 @@ module('integration/references/has-many', function (hooks) { const personsReference = family.hasMany('persons'); const data = [ - { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, - { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, + { type: 'person', id: '1', attributes: { name: 'Vito' } }, + { type: 'person', id: '2', attributes: { name: 'Michael' } }, ]; const records = await personsReference.push(data); - assert.strictEqual(get(records, 'length'), 2); + assert.strictEqual(records.length, 2); assert.strictEqual(records.at(0).name, 'Vito'); assert.strictEqual(records.at(1).name, 'Michael'); }); @@ -281,7 +290,7 @@ module('integration/references/has-many', function (hooks) { @belongsTo('family', { async: true, inverse: 'persons', as: 'person' }) family; } - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); this.owner.register('model:family', Family); this.owner.register('model:person', Person); @@ -296,10 +305,10 @@ module('integration/references/has-many', function (hooks) { const personsReference = family.hasMany('persons'); - const data = [{ data: { type: 'mafia-boss', id: '1', attributes: { name: 'Vito' } } }]; + const data = [{ type: 'mafia-boss', id: '1', attributes: { name: 'Vito' } }]; const records = await personsReference.push(data); - assert.strictEqual(get(records, 'length'), 1); + assert.strictEqual(records.length, 1); assert.strictEqual(records.at(0).name, 'Vito'); }); @@ -325,55 +334,53 @@ module('integration/references/has-many', function (hooks) { await assert.expectAssertion( async () => { - await petsReference.push([{ data: { type: 'person', id: '1' } }]); + await petsReference.push([{ type: 'person', id: '1' }]); }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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'. 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'`." ); }); - testInDebug('push(object) supports legacy, non-JSON-API-conform payload', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); + test('push valid json:api', async function (assert) { + const store = this.owner.lookup('service:store'); - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + links: { + related: '/families/1/persons', + }, + meta: { + total: 2, }, + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); + }, }); + const personsReference = family.hasMany('persons'); + const payload = { + data: [ + { type: 'person', id: '1', attributes: { name: 'Vito' } }, + { type: 'person', id: '2', attributes: { name: 'Michael' } }, + ], + }; + const pushResult = personsReference.push(payload); + assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); - var personsReference = family.hasMany('persons'); - - run(function () { - var payload = { - data: [ - { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, - { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, - ], - }; - - personsReference.push(payload).then(function (records) { - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito'); - assert.strictEqual(records.at(1).name, 'Michael'); - - done(); - }); - }); + const records = await pushResult; + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); + assert.deepEqual(personsReference.meta(), { total: 2 }, 'meta is not updated'); + assert.strictEqual(personsReference.link(), '/families/1/persons', 'link is not updated'); }); deprecatedTest( @@ -381,7 +388,7 @@ module('integration/references/has-many', function (hooks) { { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, async function (assert) { const store = this.owner.lookup('service:store'); - const deferred = defer(); + const deferred = createDeferred(); const family = store.push({ data: { @@ -398,27 +405,27 @@ module('integration/references/has-many', function (hooks) { }, }); const personsReference = family.hasMany('persons'); - let pushResult = personsReference.push(deferred.promise); + const pushResult = personsReference.push(deferred.promise); assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); const payload = { data: [ - { data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }, - { data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }, + { type: 'person', id: '1', attributes: { name: 'Vito' } }, + { type: 'person', id: '2', attributes: { name: 'Michael' } }, ], }; deferred.resolve(payload); const records = await pushResult; - assert.strictEqual(get(records, 'length'), 2); + assert.strictEqual(records.length, 2); assert.strictEqual(records.at(0).name, 'Vito'); assert.strictEqual(records.at(1).name, 'Michael'); } ); - test('push valid json:api', async function (assert) { + test('push(document) can update links', async function (assert) { const store = this.owner.lookup('service:store'); const family = store.push({ @@ -427,6 +434,7 @@ module('integration/references/has-many', function (hooks) { id: '1', relationships: { persons: { + links: { related: '/families/1/persons' }, data: [ { type: 'person', id: '1' }, { type: 'person', id: '2' }, @@ -436,40 +444,142 @@ module('integration/references/has-many', function (hooks) { }, }); const personsReference = family.hasMany('persons'); - const payload = { - data: [ - { type: 'person', id: '1', attributes: { name: 'Vito' } }, - { type: 'person', id: '2', attributes: { name: 'Michael' } }, - ], - }; - const pushResult = personsReference.push(payload); - assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.strictEqual(personsReference.link(), '/families/1/persons', 'link is correct'); - const records = await pushResult; - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito'); - assert.strictEqual(records.at(1).name, 'Michael'); + await personsReference.push( + { + links: { related: '/families/1/persons?page=1' }, + data: [ + { type: 'person', id: '3' }, + { type: 'person', id: '4' }, + ], + }, + true + ); + + assert.arrayStrictEquals(personsReference.ids(), ['3', '4'], 'ids are correct'); + assert.strictEqual(personsReference.link(), '/families/1/persons?page=1', 'link is correct'); + }); + test('push(document) can update links even when no data is present', async function (assert) { + const store = this.owner.lookup('service:store'); + + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + links: { related: '/families/1/persons' }, + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + }); + const personsReference = family.hasMany('persons'); + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.strictEqual(personsReference.link(), '/families/1/persons', 'link is correct'); + + await personsReference.push( + { + links: { related: '/families/1/persons?page=1' }, + }, + true + ); + + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.strictEqual(personsReference.link(), '/families/1/persons?page=1', 'link is correct'); + }); + test('push(document) can update meta', async function (assert) { + const store = this.owner.lookup('service:store'); + + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + meta: { total: 2 }, + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + }); + const personsReference = family.hasMany('persons'); + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.deepEqual(personsReference.meta(), { total: 2 }, 'meta is correct'); + + await personsReference.push( + { + meta: { total: 4 }, + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + { type: 'person', id: '3' }, + { type: 'person', id: '4' }, + ], + }, + true + ); + + assert.arrayStrictEquals(personsReference.ids(), ['1', '2', '3', '4'], 'ids are correct'); + assert.deepEqual(personsReference.meta(), { total: 4 }, 'meta is correct'); + }); + test('push(document) can update meta even when no data is present', async function (assert) { + const store = this.owner.lookup('service:store'); + + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + meta: { total: 2 }, + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + }); + const personsReference = family.hasMany('persons'); + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.deepEqual(personsReference.meta(), { total: 2 }, 'meta is correct'); + + await personsReference.push( + { + meta: { total: 4 }, + }, + true + ); + + assert.arrayStrictEquals(personsReference.ids(), ['1', '2'], 'ids are correct'); + assert.deepEqual(personsReference.meta(), { total: 4 }, 'meta is correct'); }); test('value() returns null when reference is not yet loaded', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); + }, }); var personsReference = family.hasMany('persons'); @@ -477,97 +587,80 @@ module('integration/references/has-many', function (hooks) { }); test('value() returns the referenced records when all records are loaded', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); - store.push({ data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }); - store.push({ data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }); + }, }); + store.push({ data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }); + store.push({ data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }); - run(function () { - var personsReference = family.hasMany('persons'); - var records = personsReference.value(); - assert.strictEqual(get(records, 'length'), 2); - assert.true(records.every((v) => v.isLoaded)); - }); + var personsReference = family.hasMany('persons'); + var records = personsReference.value(); + assert.strictEqual(records.length, 2); + assert.true(records.every((v) => v.isLoaded)); }); test('value() returns an empty array when the reference is loaded and empty', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [], }, }, - }); + }, }); - run(function () { - var personsReference = family.hasMany('persons'); - var records = personsReference.value(); - assert.strictEqual(get(records, 'length'), 0); - }); + var personsReference = family.hasMany('persons'); + var records = personsReference.value(); + assert.strictEqual(records.length, 0); }); test('_isLoaded() returns an true array when the reference is loaded and empty', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [], }, }, - }); + }, }); - run(function () { - var personsReference = family.hasMany('persons'); - var isLoaded = personsReference._isLoaded(); - assert.true(isLoaded); - }); + var personsReference = family.hasMany('persons'); + var isLoaded = personsReference._isLoaded(); + assert.true(isLoaded); }); - test('load() fetches the referenced records', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('load() fetches the referenced records', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; adapter.findMany = function (store, type, id, snapshots) { assert.strictEqual(snapshots[0].adapterOptions, adapterOptions, 'adapterOptions are passed in'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Vito' } }, { id: '2', type: 'person', attributes: { name: 'Michael' } }, @@ -575,42 +668,32 @@ module('integration/references/has-many', function (hooks) { }); }; - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); + }, }); var personsReference = family.hasMany('persons'); - run(function () { - personsReference.load({ adapterOptions }).then(function (records) { - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito'); - assert.strictEqual(records.at(1).name, 'Michael'); - - done(); - }); - }); + const records = await personsReference.load({ adapterOptions }); + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); }); - test('load() fetches link when remoteType is link', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('load() fetches link when remoteType is link', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -618,7 +701,7 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); assert.strictEqual(link, '/families/1/persons'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Vito' } }, { id: '2', type: 'person', attributes: { name: 'Michael' } }, @@ -626,38 +709,30 @@ module('integration/references/has-many', function (hooks) { }); }; - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - links: { related: '/families/1/persons' }, - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + links: { related: '/families/1/persons' }, }, }, - }); + }, }); var personsReference = family.hasMany('persons'); assert.strictEqual(personsReference.remoteType(), 'link'); - run(function () { - personsReference.load({ adapterOptions }).then(function (records) { - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito'); - assert.strictEqual(records.at(1).name, 'Michael'); - - done(); - }); - }); + const records = await personsReference.load({ adapterOptions }); + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); }); - test('load() fetches link when remoteType is link but an empty set of records is returned', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('load() fetches link when remoteType is link but an empty set of records is returned', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -665,40 +740,35 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(snapshot.adapterOptions, adapterOptions, 'adapterOptions are passed in'); assert.strictEqual(link, '/families/1/persons'); - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); }; - let family; - run(() => { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - links: { related: '/families/1/persons' }, - }, + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + links: { related: '/families/1/persons' }, }, }, - }); + }, }); - let personsReference = family.hasMany('persons'); + const personsReference = family.hasMany('persons'); assert.strictEqual(personsReference.remoteType(), 'link'); - return run(() => { - return personsReference.load({ adapterOptions }).then((records) => { - assert.strictEqual(get(records, 'length'), 0); - assert.strictEqual(get(personsReference.value(), 'length'), 0); - }); + await personsReference.load({ adapterOptions }).then((records) => { + assert.strictEqual(records.length, 0); + assert.strictEqual(personsReference.value()?.length, 0); }); }); test('load() - only a single find is triggered', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let resolveRequest; - let defered = new Promise((resolve) => { + const defered = new Promise((resolve) => { resolveRequest = resolve; }); let count = 0; @@ -745,17 +815,15 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(count, 1, 'we only requested records once'); }); - test('reload()', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('reload()', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; adapter.findMany = function (store, type, id, snapshots) { assert.strictEqual(snapshots[0].adapterOptions, adapterOptions, 'adapterOptions are passed in'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Vito Coreleone' } }, { id: '2', type: 'person', attributes: { name: 'Michael Coreleone' } }, @@ -763,44 +831,34 @@ module('integration/references/has-many', function (hooks) { }); }; - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - data: [ - { type: 'person', id: '1' }, - { type: 'person', id: '2' }, - ], - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], }, }, - }); - store.push({ data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }); - store.push({ data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }); + }, }); + store.push({ data: { type: 'person', id: '1', attributes: { name: 'Vito' } } }); + store.push({ data: { type: 'person', id: '2', attributes: { name: 'Michael' } } }); var personsReference = family.hasMany('persons'); - run(function () { - personsReference.reload({ adapterOptions }).then(function (records) { - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito Coreleone'); - assert.strictEqual(records.at(1).name, 'Michael Coreleone'); - - done(); - }); - }); + const records = await personsReference.reload({ adapterOptions }); + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito Coreleone'); + assert.strictEqual(records.at(1).name, 'Michael Coreleone'); }); - test('reload() fetches link when remoteType is link', function (assert) { - var done = assert.async(); - - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('reload() fetches link when remoteType is link', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const adapterOptions = { thing: 'one' }; @@ -811,14 +869,14 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(link, '/families/1/persons'); if (count === 1) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Vito' } }, { id: '2', type: 'person', attributes: { name: 'Michael' } }, ], }); } else { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'person', attributes: { name: 'Vito Coreleone' } }, { id: '2', type: 'person', attributes: { name: 'Michael Coreleone' } }, @@ -827,45 +885,38 @@ module('integration/references/has-many', function (hooks) { } }; - var family; - run(function () { - family = store.push({ - data: { - type: 'family', - id: '1', - relationships: { - persons: { - links: { related: '/families/1/persons' }, - }, + var family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + links: { related: '/families/1/persons' }, }, }, - }); + }, }); var personsReference = family.hasMany('persons'); assert.strictEqual(personsReference.remoteType(), 'link'); - run(function () { - personsReference - .load({ adapterOptions }) - .then(function () { - return personsReference.reload({ adapterOptions }); - }) - .then(function (records) { - assert.strictEqual(get(records, 'length'), 2); - assert.strictEqual(records.at(0).name, 'Vito Coreleone'); - assert.strictEqual(records.at(1).name, 'Michael Coreleone'); - - done(); - }); - }); + await personsReference + .load({ adapterOptions }) + .then(function () { + return personsReference.reload({ adapterOptions }); + }) + .then(function (records) { + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito Coreleone'); + assert.strictEqual(records.at(1).name, 'Michael Coreleone'); + }); }); test('push record with nested includes (async has-many), chained HasManyReference#value()', async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let family = store.push({ + const family = store.push({ data: { type: 'family', id: '1', @@ -946,10 +997,10 @@ module('integration/references/has-many', function (hooks) { ], }); - let persons = family.hasMany('persons').value(); + const persons = family.hasMany('persons').value(); assert.strictEqual(persons.length, 2); persons.forEach((person) => { - let pets = person.hasMany('pets').value(); + const pets = person.hasMany('pets').value(); assert.strictEqual(pets.length, 2); }); }); @@ -957,11 +1008,11 @@ module('integration/references/has-many', function (hooks) { test('fetch record with nested includes (async has-many), chained HasManyReference#value', async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshots) { - return resolve({ + return Promise.resolve({ data: { type: 'family', id: '1', @@ -1043,11 +1094,11 @@ module('integration/references/has-many', function (hooks) { }); }; - let family = await store.findRecord('family', '1'); - let persons = family.hasMany('persons').value(); + const family = await store.findRecord('family', '1'); + const persons = family.hasMany('persons').value(); assert.strictEqual(persons.length, 2); persons.forEach((person) => { - let pets = person.hasMany('pets').value(); + const pets = person.hasMany('pets').value(); assert.strictEqual(pets.length, 2); }); }); diff --git a/tests/main/tests/integration/references/record-test.js b/tests/main/tests/integration/references/record-test.js index 2aa90c274ed..81656955983 100644 --- a/tests/main/tests/integration/references/record-test.js +++ b/tests/main/tests/integration/references/record-test.js @@ -1,12 +1,12 @@ import { get } from '@ember/object'; import { module, test } from 'qunit'; -import { defer, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import { recordIdentifierFor } from '@ember-data/store'; @@ -24,8 +24,8 @@ module('integration/references/record', function (hooks) { }); test('a RecordReference can be retrieved via store.getReference(type, id)', function (assert) { - let store = this.owner.lookup('service:store'); - let recordReference = store.getReference('person', 1); + const store = this.owner.lookup('service:store'); + const recordReference = store.getReference('person', 1); assert.strictEqual(recordReference.remoteType(), 'identity'); assert.strictEqual(recordReference.type, 'person'); @@ -33,8 +33,8 @@ module('integration/references/record', function (hooks) { }); test('a RecordReference can be retrieved via store.getReference(identifier) without local state', function (assert) { - let store = this.owner.lookup('service:store'); - let recordReference = store.getReference({ type: 'person', id: '1' }); + const store = this.owner.lookup('service:store'); + const recordReference = store.getReference({ type: 'person', id: '1' }); assert.strictEqual(recordReference.remoteType(), 'identity'); assert.strictEqual(recordReference.type, 'person'); @@ -47,7 +47,7 @@ module('integration/references/record', function (hooks) { { withType: true, withLid: true, isCreate: true, desc: 'type and lid via store.createRecord (no local id)' }, ].forEach(({ withType, withId, withLid, isCreate, desc }) => { test(`a RecordReference can be retrieved with ${desc}`, function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let person; if (isCreate) { // no id @@ -71,7 +71,7 @@ module('integration/references/record', function (hooks) { } } - let recordReference = store.getReference(getReferenceArgs); + const recordReference = store.getReference(getReferenceArgs); assert.strictEqual(recordReference.remoteType(), 'identity'); assert.strictEqual(recordReference.type, 'person'); @@ -84,10 +84,10 @@ module('integration/references/record', function (hooks) { }); test('push(object)', async function (assert) { - let store = this.owner.lookup('service:store'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const Person = store.modelFor('person'); - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); const pushed = recordReference.push({ data: { @@ -101,19 +101,19 @@ module('integration/references/record', function (hooks) { assert.ok(pushed.then, 'RecordReference.push returns a promise'); - let record = await pushed; + const record = await pushed; assert.ok(record instanceof Person, 'push resolves with the record'); assert.strictEqual(get(record, 'name'), 'le name'); }); test('push(promise)', async function (assert) { - let store = this.owner.lookup('service:store'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const Person = store.modelFor('person'); - let deferred = defer(); - let recordReference = store.getReference('person', 1); + const deferred = createDeferred(); + const recordReference = store.getReference('person', 1); - let pushed = recordReference.push(deferred.promise); + const pushed = recordReference.push(deferred.promise); assert.ok(pushed.then, 'RecordReference.push returns a promise'); @@ -127,37 +127,37 @@ module('integration/references/record', function (hooks) { }, }); - let record = await pushed; + const record = await pushed; assert.ok(record instanceof Person, 'push resolves with the record'); assert.strictEqual(get(record, 'name'), 'le name', 'name is updated'); }); test('value() returns null when not yet loaded', function (assert) { - let store = this.owner.lookup('service:store'); - let recordReference = store.getReference('person', 1); + const store = this.owner.lookup('service:store'); + const recordReference = store.getReference('person', 1); assert.strictEqual(recordReference.value(), null); }); test('value() returns the record when loaded', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', }, }); - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); assert.strictEqual(recordReference.value(), person); }); test('load() fetches the record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -168,22 +168,22 @@ module('integration/references/record', function (hooks) { }); }; - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); - let record = await recordReference.load(); + const record = await recordReference.load(); assert.strictEqual(get(record, 'name'), 'Vito'); }); test('load() only a single find is triggered', async function (assert) { assert.expect(3); let resolveRequest; - let deferred = new Promise((resolve) => { + const deferred = new Promise((resolve) => { resolveRequest = resolve; }); let count = 0; - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldReloadRecord = function () { return false; @@ -198,10 +198,10 @@ module('integration/references/record', function (hooks) { return deferred; }; - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); recordReference.load(); // first trigger - let recordPromise = recordReference.load(); // second trigger + const recordPromise = recordReference.load(); // second trigger resolveRequest({ data: { id: '1', @@ -219,15 +219,15 @@ module('integration/references/record', function (hooks) { }); test('reload() loads the record if not yet loaded', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let count = 0; adapter.findRecord = function (store, type, id) { count++; assert.strictEqual(count, 1); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -238,18 +238,18 @@ module('integration/references/record', function (hooks) { }); }; - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); - let record = await recordReference.reload(); + const record = await recordReference.reload(); assert.strictEqual(get(record, 'name'), 'Vito Coreleone'); }); test('reload() fetches the record', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -270,9 +270,9 @@ module('integration/references/record', function (hooks) { }, }); - let recordReference = store.getReference('person', 1); + const recordReference = store.getReference('person', 1); - let record = await recordReference.reload(); + const record = await recordReference.reload(); assert.strictEqual(get(record, 'name'), 'Vito Coreleone'); }); }); diff --git a/tests/main/tests/integration/relationships/belongs-to-test.js b/tests/main/tests/integration/relationships/belongs-to-test.js index 1b8e22cd62e..df839881df4 100644 --- a/tests/main/tests/integration/relationships/belongs-to-test.js +++ b/tests/main/tests/integration/relationships/belongs-to-test.js @@ -1,17 +1,15 @@ import EmberObject from '@ember/object'; -import { run } from '@ember/runloop'; import { module, test } from 'qunit'; -import { hash, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; 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'; @@ -34,7 +32,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' } hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('model:pet', Pet); owner.register( @@ -62,7 +60,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' 'adapter:company', JSONAPIAdapter.extend({ findBelongsTo(store, type, snapshot) { - return resolve({ + return Promise.resolve({ links: { related: 'company/1/parent-company', }, @@ -95,7 +93,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' } catch (e) { assert.strictEqual( e.message, - `Assertion Failed: fetched the belongsTo relationship 'parentCompany' for company:1 with link 'company/1/parent-company', but no data member is present in the response. If no data exists, the response should set { data: null }`, + `fetched the belongsTo relationship 'parentCompany' for company:1 with link '"company/1/parent-company"', but no data member is present in the response. If no data exists, the response should set { data: null }`, 'We error appropriately' ); } @@ -115,7 +113,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' 'adapter:company', JSONAPIAdapter.extend({ createRecord(store, type, snapshot) { - return resolve({ + return Promise.resolve({ data: { type: 'company', id: '123', @@ -131,7 +129,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' }) ); - let company = store.createRecord('company', { name: 'Acme Corporation' }); + const company = store.createRecord('company', { name: 'Acme Corporation' }); await company.save(); assert.strictEqual(company.id, '123', 'We updated to the correct id'); assert.strictEqual(company.belongsTo('parentCompany').id(), company.id, 'We are able to reference ourselves'); @@ -144,7 +142,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' JSONAPIAdapter.extend({ findRecord() { assert.strictEqual(++petFindRecordCalls, 1, 'We call findRecord only once for our pet'); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '1', @@ -168,7 +166,7 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' JSONAPIAdapter.extend({ findRecord() { assert.strictEqual(++personFindRecordCalls, 1, 'We call findRecord only once for our person'); - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -190,17 +188,17 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' }) ); - let person = await store.findRecord('person', '1'); - let petRequest = store.findRecord('pet', '1'); - let personPetRequest = person.bestDog; - let personPet = await personPetRequest; - let pet = await petRequest; + const person = await store.findRecord('person', '1'); + const petRequest = store.findRecord('pet', '1'); + const personPetRequest = person.bestDog; + const personPet = await personPetRequest; + const pet = await petRequest; assert.strictEqual(personPet, pet, 'We ended up in the same state'); }); test('async belongsTo returns correct new value after a local change', async function (assert) { - let chris = store.push({ + const chris = store.push({ data: { type: 'person', id: '1', @@ -235,8 +233,8 @@ module('integration/relationship/belongs-to BelongsTo Relationships (new-style)' ], }); - let shen = store.peekRecord('pet', '1'); - let pirate = store.peekRecord('pet', '2'); + const shen = store.peekRecord('pet', '1'); + const pirate = store.peekRecord('pet', '2'); let bestDog = await chris.bestDog; assert.strictEqual(shen.bestHuman, null, 'precond - Shen has no best human'); @@ -397,8 +395,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }) ); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -444,7 +442,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.shouldBackgroundReloadRecord = () => false; adapter.updateRecord = (store, type, snapshot) => { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'app', @@ -471,8 +469,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function ); }); - test('The store can materialize a non loaded monomorphic belongsTo association', function (assert) { - assert.expect(1); + test('The store can materialize a non loaded monomorphic belongsTo association', async function (assert) { + assert.expect(2); class Post extends Model { @attr('string') title; @@ -481,13 +479,13 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:post', Post); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = function (store, type, id, snapshot) { assert.ok(true, "The adapter's find method should be called"); - return resolve({ + return Promise.resolve({ data: { id, type: snapshot.modelName, @@ -495,89 +493,81 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }; - run(() => { - store.push({ - data: { - id: '1', - type: 'post', - relationships: { - user: { - data: { - id: '2', - type: 'user', - }, + store.push({ + data: { + id: '1', + type: 'post', + relationships: { + user: { + data: { + id: '2', + type: 'user', }, }, }, - }); + }, }); - return run(() => { - return store.findRecord('post', 1).then((post) => { - post.user; - }); - }); + const post = await store.findRecord('post', '1'); + const user = await post.user; + assert.strictEqual(user.id, '2', 'The post should have a user now'); }); testInDebug('Invalid belongsTo relationship identifiers throw errors for null id', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // test null id assert.expectAssertion(() => { - run(() => { - let post = store.push({ - data: { - id: '1', - type: 'post', - relationships: { - user: { - data: { - id: null, - type: 'user', - }, + const post = store.push({ + data: { + id: '1', + type: 'post', + relationships: { + user: { + data: { + id: null, + type: 'user', }, }, }, - }); - post.user; + }, }); - }, /Assertion Failed: Encountered a relationship identifier without an id for the belongsTo relationship 'user' on , expected an identifier but found/); + post.user; + }, /Encountered a relationship identifier without an id for the belongsTo relationship 'user' on , expected an identifier but found/); }); testInDebug('Invalid belongsTo relationship identifiers throw errors for null type', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // test missing type assert.expectAssertion(() => { - run(() => { - let post = store.push({ - data: { - id: '2', - type: 'post', - relationships: { - user: { - data: { - id: '1', - type: null, - }, + const post = store.push({ + data: { + id: '2', + type: 'post', + relationships: { + user: { + data: { + id: '1', + type: null, }, }, }, - }); - post.user; + }, }); - }, /Assertion Failed: Encountered a relationship identifier without a type for the belongsTo relationship 'user' on , expected an identifier with type 'user' but found/); + post.user; + }, /Encountered a relationship identifier without a type for the belongsTo relationship 'user' on , expected an identifier with type 'user' but found/); }); testInDebug( 'Only a record of the same modelClass can be used with a monomorphic belongsTo relationship', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const post = store.push({ data: { @@ -597,127 +587,116 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function post.user = comment; }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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'. 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'`." ); } ); - test('The store can load a polymorphic belongsTo association', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('The store can load a polymorphic belongsTo association', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - run(() => { - store.push({ - data: { - id: '1', - type: 'post', - }, - }); + store.push({ + data: { + id: '1', + type: 'post', + }, + }); - store.push({ - data: { - id: '2', - type: 'comment', - relationships: { - message: { - data: { - id: '1', - type: 'post', - }, + store.push({ + data: { + id: '2', + type: 'comment', + relationships: { + message: { + data: { + id: '1', + type: 'post', }, }, }, - }); + }, }); - return run(() => { - return hash({ - message: store.findRecord('post', 1), - comment: store.findRecord('comment', 2), - }).then((records) => { - assert.strictEqual(records.comment.message, records.message); - }); - }); + const [message, comment] = await Promise.all([store.findRecord('post', '1'), store.findRecord('comment', '2')]); + + assert.strictEqual(comment.message, message); }); - test('The store can serialize a polymorphic belongsTo association', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('The store can serialize a polymorphic belongsTo association', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - let serializerInstance = store.serializerFor('comment'); + const serializerInstance = store.serializerFor('comment'); serializerInstance.serializePolymorphicType = function (record, json, relationship) { assert.ok(true, "The serializer's serializePolymorphicType method should be called"); json['message_type'] = 'post'; }; - return run(() => { - store.push({ - data: { - id: '1', - type: 'post', - }, - }); + store.push({ + data: { + id: '1', + type: 'post', + }, + }); - store.push({ - data: { - id: '2', - type: 'comment', - relationships: { - message: { - data: { - id: '1', - type: 'post', - }, + store.push({ + data: { + id: '2', + type: 'comment', + relationships: { + message: { + data: { + id: '1', + type: 'post', }, }, }, - }); + }, + }); - return store.findRecord('comment', 2).then((comment) => { - let serialized = comment.serialize({ includeId: true }); - assert.strictEqual(serialized.data.relationships.message.data.id, '1'); - assert.strictEqual(serialized.data.relationships.message.data.type, 'posts'); - }); + await store.findRecord('comment', '2').then((comment) => { + const serialized = comment.serialize({ includeId: true }); + assert.strictEqual(serialized.data.relationships.message.data.id, '1'); + assert.strictEqual(serialized.data.relationships.message.data.type, 'posts'); }); }); - test('A serializer can materialize a belongsTo as a link that gets sent back to findBelongsTo', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('A serializer can materialize a belongsTo as a link that gets sent back to findBelongsTo', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - let Group = Model.extend({ + const Group = Model.extend({ people: hasMany('person', { async: false, inverse: 'group' }), }); - let Person = Model.extend({ + const Person = Model.extend({ group: belongsTo('group', { async: true, inverse: 'people' }), }); this.owner.register('model:group', Group); this.owner.register('model:person', Person); - run(() => { - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - group: { - links: { - related: '/people/1/group', - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + group: { + links: { + related: '/people/1/group', }, }, }, - }); + }, }); adapter.findRecord = function (store, type, id, snapshot) { @@ -726,10 +705,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function adapter.findBelongsTo = function (store, snapshot, link, relationship) { assert.strictEqual(relationship.type, 'group'); - assert.strictEqual(relationship.key, 'group'); + assert.strictEqual(relationship.name, 'group'); assert.strictEqual(link, '/people/1/group'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'group', @@ -742,50 +721,46 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }; - return run(() => { - return store - .findRecord('person', 1) - .then((person) => { - return person.group; - }) - .then((group) => { - assert.ok(group instanceof Group, 'A group object is loaded'); - assert.strictEqual(group.id, '1', 'It is the group we are expecting'); - }); - }); + await store + .findRecord('person', 1) + .then((person) => { + return person.group; + }) + .then((group) => { + assert.ok(group instanceof Group, 'A group object is loaded'); + assert.strictEqual(group.id, '1', 'It is the group we are expecting'); + }); }); - test('A record with an async belongsTo relationship always returns a promise for that relationship', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('A record with an async belongsTo relationship always returns a promise for that relationship', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - let Seat = Model.extend({ + const Seat = Model.extend({ person: belongsTo('person', { async: false, inverse: 'seat' }), }); - let Person = Model.extend({ + const Person = Model.extend({ seat: belongsTo('seat', { async: true, inverse: 'person' }), }); this.owner.register('model:seat', Seat); this.owner.register('model:person', Person); - run(() => { - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - seat: { - links: { - related: '/people/1/seat', - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + seat: { + links: { + related: '/people/1/seat', }, }, }, - }); + }, }); adapter.findRecord = function (store, type, id, snapshot) { @@ -793,54 +768,50 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; adapter.findBelongsTo = function (store, snapshot, link, relationship) { - return resolve({ data: { id: '1', type: 'seat' } }); + return Promise.resolve({ data: { id: '1', type: 'seat' } }); }; - return run(() => { - return store.findRecord('person', 1).then((person) => { - return person.seat.then((seat) => { - // this assertion fails too - // ok(seat.person === person, 'parent relationship should be populated'); - seat.set('person', person); - assert.ok(person.seat.then, 'seat should be a PromiseObject'); - }); + await store.findRecord('person', '1').then((person) => { + return person.seat.then((seat) => { + // this assertion fails too + // ok(seat.person === person, 'parent relationship should be populated'); + seat.set('person', person); + assert.ok(person.seat.then, 'seat should be a PromiseObject'); }); }); }); - test('A record with an async belongsTo relationship returning null should resolve null', function (assert) { + test('A record with an async belongsTo relationship returning null should resolve null', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - let Group = Model.extend({ + const Group = Model.extend({ people: hasMany('person', { async: false, inverse: 'group' }), }); - let Person = Model.extend({ + const Person = Model.extend({ group: belongsTo('group', { async: true, inverse: 'people' }), }); this.owner.register('model:group', Group); this.owner.register('model:person', Person); - run(() => { - store.push({ - data: { - id: '1', - type: 'person', - relationships: { - group: { - links: { - related: '/people/1/group', - }, + store.push({ + data: { + id: '1', + type: 'person', + relationships: { + group: { + links: { + related: '/people/1/group', }, }, }, - }); + }, }); adapter.findRecord = function (store, type, id, snapshot) { @@ -848,10 +819,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; adapter.findBelongsTo = function (store, snapshot, link, relationship) { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }; - return store + await store .findRecord('person', '1') .then((person) => { return person.group; @@ -867,16 +838,16 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - let Group = Model.extend({ + const Group = Model.extend({ people: hasMany('person', { async: false, inverse: 'group' }), }); - let Person = Model.extend({ + const Person = Model.extend({ group: belongsTo('group', { async: true, inverse: 'people' }), }); @@ -897,9 +868,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, }); - let groupPromise = originalOwner.group; + const groupPromise = originalOwner.group; const group = await groupPromise; - let person = store.createRecord('person', { + const person = store.createRecord('person', { group: groupPromise, }); const personGroup = await person.group; @@ -910,16 +881,14 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('polymorphic belongsTo class-checks check the superclass', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(() => { - let igor = store.createRecord('user', { name: 'Igor' }); - let post = store.createRecord('post', { title: "Igor's unimaginative blog post" }); + const igor = store.createRecord('user', { name: 'Igor' }); + const post = store.createRecord('post', { title: "Igor's unimaginative blog post" }); - igor.set('favouriteMessage', post); + igor.set('favouriteMessage', post); - assert.strictEqual(igor.favouriteMessage.title, "Igor's unimaginative blog post"); - }); + assert.strictEqual(igor.favouriteMessage.title, "Igor's unimaginative blog post"); }); test('relationship changes shouldn’t cause async fetches', async function (assert) { @@ -938,8 +907,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function * - comment is destroyed */ - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Message extends Model { @attr('date') created_at; @@ -1020,8 +989,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('Destroying a record with an unloaded aync belongsTo association does not fetch the record', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); class Post extends Model { @attr('string') title; @@ -1078,24 +1047,21 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); testInDebug('A sync belongsTo errors out if the record is unloaded', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let message; - run(() => { - message = store.push({ - data: { - id: '1', - type: 'message', - relationships: { - user: { - data: { - id: '2', - type: 'user', - }, + const message = store.push({ + data: { + id: '1', + type: 'message', + relationships: { + user: { + data: { + id: '2', + type: 'user', }, }, }, - }); + }, }); assert.expectAssertion(() => { @@ -1111,9 +1077,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let book, author; - book = store.push({ + const store = this.owner.lookup('service:store'); + const book = store.push({ data: { id: '1', type: 'book', @@ -1130,7 +1095,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, }, }); - author = store.push({ + const author = store.push({ data: { id: '2', type: 'author', @@ -1149,10 +1114,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test('Rollbacking attributes for a deleted record restores implicit relationship - sync', function (assert) { - let store = this.owner.lookup('service:store'); - let book, author; + const store = this.owner.lookup('service:store'); - book = store.push({ + const book = store.push({ data: { id: '1', type: 'book', @@ -1170,7 +1134,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, }); - author = store.push({ + const author = store.push({ data: { id: '2', type: 'author', @@ -1207,11 +1171,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function @hasMany('chapter', { async: false, inverse: 'book' }) chapters; } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1224,7 +1188,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; await store.findRecord('book', 1).then((book) => { - let relationship = getRelationshipStateForRecord(book, 'author'); + const relationship = getRelationshipStateForRecord(book, 'author'); assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); @@ -1232,11 +1196,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('belongsTo hasAnyRelationshipData sync loaded', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1249,7 +1213,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; await store.findRecord('book', 1).then((book) => { - let relationship = getRelationshipStateForRecord(book, 'author'); + const relationship = getRelationshipStateForRecord(book, 'author'); assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); @@ -1262,11 +1226,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function @hasMany('chapter', { async: false, inverse: 'book' }) chapters; } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1279,7 +1243,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; await store.findRecord('book', 1).then((book) => { - let relationship = getRelationshipStateForRecord(book, 'author'); + const relationship = getRelationshipStateForRecord(book, 'author'); assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); @@ -1287,11 +1251,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('belongsTo hasAnyRelationshipData sync not loaded', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1301,7 +1265,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; await store.findRecord('book', 1).then((book) => { - let relationship = getRelationshipStateForRecord(book, 'author'); + const relationship = getRelationshipStateForRecord(book, 'author'); assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); @@ -1314,9 +1278,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function @hasMany('chapter', { async: false, inverse: 'book' }) chapters; } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let author = store.createRecord('author'); + const author = store.createRecord('author'); let book = store.createRecord('book', { name: 'The Greatest Book' }); let relationship = getRelationshipStateForRecord(book, 'author'); @@ -1335,9 +1299,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function test('belongsTo hasAnyRelationshipData sync created', function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let author = store.createRecord('author'); + const author = store.createRecord('author'); let book = store.createRecord('book', { name: 'The Greatest Book', }); @@ -1355,10 +1319,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test("Model's belongsTo relationship should not be created during model creation", function (assert) { - let store = this.owner.lookup('service:store'); - let user; + const store = this.owner.lookup('service:store'); - user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -1372,9 +1335,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test("Model's belongsTo relationship should be created during model creation if relationship passed in constructor", function (assert) { - let store = this.owner.lookup('service:store'); - let message = store.createRecord('message'); - let user = store.createRecord('user', { + const store = this.owner.lookup('service:store'); + const message = store.createRecord('message'); + const user = store.createRecord('user', { name: 'John Doe', favouriteMessage: message, }); @@ -1386,11 +1349,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test("Model's belongsTo relationship should be created during 'set' method", function (assert) { - let store = this.owner.lookup('service:store'); - let user, message; + const store = this.owner.lookup('service:store'); - message = store.createRecord('message'); - user = store.createRecord('user'); + const message = store.createRecord('message'); + const user = store.createRecord('user'); user.set('favouriteMessage', message); assert.ok( hasRelationshipForRecord(user, 'favouriteMessage'), @@ -1399,10 +1361,9 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test("Model's belongsTo relationship should be created during 'get' method", function (assert) { - let store = this.owner.lookup('service:store'); - let user; + const store = this.owner.lookup('service:store'); - user = store.createRecord('user'); + const user = store.createRecord('user'); user.favouriteMessage; assert.ok( hasRelationshipForRecord(user, 'favouriteMessage'), @@ -1419,13 +1380,13 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findBelongsTo = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'author', 'url is correct'); assert.ok(true, "The adapter's findBelongsTo method should be called"); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'author', @@ -1434,7 +1395,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1463,12 +1424,12 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findBelongsTo = function (store, snapshot, url, relationship) { assert.ok(true, "The adapter's findBelongsTo method should be called"); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'author', @@ -1481,7 +1442,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.ok(false, "The adapter's findRecord method should not be called"); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1511,8 +1472,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -1525,7 +1486,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.ok(false, "The adapter's findRecord method should not be called"); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1562,13 +1523,13 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findBelongsTo = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'author-new-link', 'url is correct'); assert.ok(true, "The adapter's findBelongsTo method should be called"); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'author', @@ -1581,7 +1542,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.ok(false, "The adapter's findRecord method should not be called"); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1625,13 +1586,13 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findBelongsTo = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'author-updated-link', 'url is correct'); assert.ok(true, "The adapter's findBelongsTo method should be called"); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'author', @@ -1646,7 +1607,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.ok(false, "The adapter's findRecord method should not be called"); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1705,8 +1666,8 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:book', Book); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findBelongsTo = function () { assert.ok(false, "The adapter's findBelongsTo method should not be called"); @@ -1716,7 +1677,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.ok(false, "The adapter's findRecord method should not be called"); }; - let book = store.push({ + const book = store.push({ data: { type: 'book', id: '1', @@ -1772,11 +1733,11 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'chapter', @@ -1790,7 +1751,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }; adapter.findBelongsTo = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1812,7 +1773,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.strictEqual(book.name, 'book title'); adapter.findBelongsTo = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1829,10 +1790,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); test('A synchronous belongsTo relationship can be reloaded using a reference if it was fetched via id', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let chapter = store.push({ + const chapter = store.push({ data: { type: 'chapter', id: '1', @@ -1854,7 +1815,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); adapter.findRecord = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1863,7 +1824,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }; - let book = chapter.book; + const book = chapter.book; assert.strictEqual(book.name, 'book title'); await chapter @@ -1881,10 +1842,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let chapter = store.push({ + const chapter = store.push({ data: { type: 'chapter', id: '1', @@ -1897,7 +1858,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); adapter.findRecord = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1911,7 +1872,7 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.strictEqual(book.name, 'book title'); adapter.findRecord = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'book', @@ -1928,10 +1889,10 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); testInDebug('A belongsTo relationship warns if malformatted data is pushed into the store', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async () => { - let chapter = store.push({ + const chapter = store.push({ data: { type: 'chapter', id: '1', @@ -1946,41 +1907,37 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, /Encountered a relationship identifier without a type for the belongsTo relationship 'book' on , expected an identifier with type 'book'/); }); - test("belongsTo relationship with links doesn't trigger extra change notifications - #4942", function (assert) { + test("belongsTo relationship with links doesn't trigger extra change notifications - #4942", async function (assert) { class Chapter extends Model { @attr title; @belongsTo('book', { async: true, inverse: 'chapters' }) book; } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: { - type: 'chapter', - id: '1', - relationships: { - book: { - data: { type: 'book', id: '1' }, - links: { related: '/chapter/1/book' }, - }, + store.push({ + data: { + type: 'chapter', + id: '1', + relationships: { + book: { + data: { type: 'book', id: '1' }, + links: { related: '/chapter/1/book' }, }, }, - included: [{ type: 'book', id: '1' }], - }); + }, + included: [{ type: 'book', id: '1' }], }); - let chapter = store.peekRecord('chapter', '1'); + const chapter = store.peekRecord('chapter', '1'); let count = 0; chapter.addObserver('book', () => { count++; }); - run(() => { - chapter.book; - }); + await chapter.book; assert.strictEqual(count, 0); }); @@ -2033,7 +1990,6 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function const friend = await bestFriendPromise; const enemy = await worstEnemyPromise; - // eslint-disable-next-line qunit/no-ok-equality assert.true(friend !== enemy, 'we got separate records'); }); }); 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 index 1e99f0fa959..6037969b471 100644 --- a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts +++ b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts @@ -4,23 +4,22 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 } from '@ember-data/deprecations'; +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 { ExistingResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; +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 { ReactiveContext, reactiveContext } from '../../../helpers/reactive-context'; - -let IS_DEPRECATE_MANY_ARRAY_DUPLICATES = false; - -if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - IS_DEPRECATE_MANY_ARRAY_DUPLICATES = true; -} +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: User[]; + @hasMany('user', { async: false, inverse: 'friends' }) declare friends: ManyArray; + + [Type] = 'user' as const; } function krystanData() { @@ -209,7 +208,11 @@ async function applyMutation(assert: Assert, store: Store, record: User, mutatio const result = generateAppliedMutation(store, record, mutation); const initialIds = record.friends.map((f) => f.id).join(','); - const shouldError = result.hasDuplicates && !IS_DEPRECATE_MANY_ARRAY_DUPLICATES; + 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 { @@ -229,11 +232,27 @@ async function applyMutation(assert: Assert, store: Store, record: User, mutatio 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)) @@ -388,7 +407,11 @@ module('Integration | Relationships | Collection | Mutation', function (hooks) { 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, [{ name: 'friends', type: 'hasMany' }]); + 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); diff --git a/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js b/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js index ccc6e02665f..998414e1719 100644 --- a/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js +++ b/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js @@ -357,16 +357,21 @@ module('Integration | Relationships | Explicit Polymorphic BelongsTo', function [ 'taggable', { - tag: { - kind: 'belongsTo', - type: 'tag', - name: 'tag', - options: { - async: false, - inverse: 'tagged', - as: 'taggable', - }, - }, + fields: new Map([ + [ + 'tag', + { + kind: 'belongsTo', + type: 'tag', + name: 'tag', + options: { + async: false, + inverse: 'tagged', + as: 'taggable', + }, + }, + ], + ]), }, ], ]); @@ -376,24 +381,23 @@ module('Integration | Relationships | Explicit Polymorphic BelongsTo', function this._schema = schema; } - doesTypeExist(type) { + hasResource({ type }) { if (AbstractSchemas.has(type)) { return true; // some apps may want `true` } - return this._schema.doesTypeExist(type); + return this._schema.hasResource({ type }); } - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); - } - - relationshipsDefinitionFor(identifier) { + fields(identifier) { const schema = AbstractSchemas.get(identifier.type); - return schema || this._schema.relationshipsDefinitionFor(identifier); + if (schema) { + return schema.fields; + } + return this._schema.fields(identifier); } } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); owner.register( 'model:tag', diff --git a/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js b/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js index 4dcf58a4c02..aaab4ffc90b 100644 --- a/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js +++ b/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js @@ -372,21 +372,25 @@ module('Integration | Relationships | Explicit Polymorphic HasMany', function (h test('a polymorphic hasMany relationship with a specified inverse can use an abstract-type defined via the schema service', async function (assert) { const { owner } = this; const store = owner.lookup('service:store'); - const AbstractSchemas = new Map([ [ 'taggable', { - tag: { - kind: 'belongsTo', - type: 'tag', - name: 'tag', - options: { - async: false, - inverse: 'tagged', - as: 'taggable', - }, - }, + fields: new Map([ + [ + 'tag', + { + kind: 'belongsTo', + type: 'tag', + name: 'tag', + options: { + async: false, + inverse: 'tagged', + as: 'taggable', + }, + }, + ], + ]), }, ], ]); @@ -396,24 +400,23 @@ module('Integration | Relationships | Explicit Polymorphic HasMany', function (h this._schema = schema; } - doesTypeExist(type) { + hasResource({ type }) { if (AbstractSchemas.has(type)) { return true; // some apps may want `true` } - return this._schema.doesTypeExist(type); - } - - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); + return this._schema.hasResource({ type }); } - relationshipsDefinitionFor(identifier) { + fields(identifier) { const schema = AbstractSchemas.get(identifier.type); - return schema || this._schema.relationshipsDefinitionFor(identifier); + if (schema) { + return schema.fields; + } + return this._schema.fields(identifier); } } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); owner.register( 'model:tag', diff --git a/tests/main/tests/integration/relationships/has-many-sort-order-test.js b/tests/main/tests/integration/relationships/has-many-sort-order-test.js index 02d45b2a4d5..3eb24d2ce02 100644 --- a/tests/main/tests/integration/relationships/has-many-sort-order-test.js +++ b/tests/main/tests/integration/relationships/has-many-sort-order-test.js @@ -3,6 +3,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; class User extends Model { @attr name; @@ -172,9 +173,9 @@ module('integration/relationships/hasMany - Sort Order', function (hooks) { class TestAdapter { updateRecord(store, type, snapshot) { assert.step('updateRecord'); - store.serializerFor(type.modelName).serialize(snapshot, { includeId: true }); + const serialized = store.serializerFor(type.modelName).serialize(snapshot, { includeId: true }); assert.step('serialized'); - return Promise.resolve(); + return Promise.resolve(serialized); } static create() { return new this(); @@ -265,16 +266,16 @@ module('integration/relationships/hasMany - Sort Order', function (hooks) { assert.verifySteps(['updateRecord', 'serializing', 'serialized'], 'serialize was called'); }); - test('hasMany reflects sort order from server after local changes are made but new server state is recieved', async function (assert) { + test('hasMany reflects sort order from local changes aeven after new server state is recieved', async function (assert) { const store = this.owner.lookup('service:store'); this.owner.register( 'adapter:application', class TestAdapter { updateRecord(store, type, snapshot) { assert.step('updateRecord'); - store.serializerFor(type.modelName).serialize(snapshot, { includeId: true }); + const serialized = store.serializerFor(type.modelName).serialize(snapshot, { includeId: true }); assert.step('serialized'); - return Promise.resolve(); + return Promise.resolve(serialized); } static create() { return new this(); @@ -378,7 +379,11 @@ module('integration/relationships/hasMany - Sort Order', function (hooks) { }); const petsAgain = user.pets.map((pet) => pet.name); - assert.deepEqual(petsAgain, ['Rex', 'Fido', 'Spot'], 'Pets are in the right order'); + if (DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { + assert.deepEqual(petsAgain, ['Rex', 'Fido', 'Spot'], 'Pets are in the right order'); + } else { + assert.deepEqual(petsAgain, ['Spot', 'Fido', 'Rex'], 'Pets are still in the right order'); + } }); test('when we remove a record and save, the api is alerted', async function (assert) { diff --git a/tests/main/tests/integration/relationships/has-many-test.js b/tests/main/tests/integration/relationships/has-many-test.js index a2dcf4944c6..85e02673651 100644 --- a/tests/main/tests/integration/relationships/has-many-test.js +++ b/tests/main/tests/integration/relationships/has-many-test.js @@ -1,20 +1,18 @@ -import { get } from '@ember/object'; -import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { all, Promise as EmberPromise, reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import RESTAdapter from '@ember-data/adapter/rest'; -import { DEPRECATE_ARRAY_LIKE, DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; 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'; @@ -108,7 +106,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( 'adapter:company', JSONAPIAdapter.extend({ findHasMany(store, type, snapshot) { - return resolve({ + return Promise.resolve({ links: { related: 'company/1/employees', }, @@ -142,7 +140,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } catch (e) { assert.strictEqual( e.message, - `Assertion Failed: fetched the hasMany relationship 'employees' for company:1 with link 'company/1/employees', but no data member is present in the response. If no data exists, the response should set { data: [] }`, + `fetched the hasMany relationship 'employees' for company:1 with link '"company/1/employees"', but no data member is present in the response. If no data exists, the response should set { data: [] }`, 'We error appropriately' ); } @@ -152,50 +150,46 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( testInDebug('Invalid hasMany relationship identifiers throw errors for missing id', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // test null id assert.expectAssertion(() => { - run(() => { - let post = store.push({ - data: { - id: '1', - type: 'post', - relationships: { - comments: { - data: [{ id: null, type: 'comment' }], - }, + const post = store.push({ + data: { + id: '1', + type: 'post', + relationships: { + comments: { + data: [{ id: null, type: 'comment' }], }, }, - }); - - post.comments; + }, }); - }, /Assertion Failed: Encountered a relationship identifier without an id for the hasMany relationship 'comments' on , expected an identifier but found/); + + post.comments; + }, /Encountered a relationship identifier without an id for the hasMany relationship 'comments' on , expected an identifier but found/); }); testInDebug('Invalid hasMany relationship identifiers throw errors for missing type', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // test missing type assert.expectAssertion(() => { - run(() => { - let post = store.push({ - data: { - id: '2', - type: 'post', - relationships: { - comments: { - data: [{ id: '1', type: null }], - }, + const post = store.push({ + data: { + id: '2', + type: 'post', + relationships: { + comments: { + data: [{ id: '1', type: null }], }, }, - }); - post.comments; + }, }); - }, /Assertion Failed: Encountered a relationship identifier without a type for the hasMany relationship 'comments' on , expected an identifier with type 'comment' but found/); + post.comments; + }, /Encountered a relationship identifier without a type for the hasMany relationship 'comments' on , expected an identifier with type 'comment' but found/); }); test('A record with an async hasMany relationship can safely be saved and later access the relationship', async function (assert) { @@ -211,7 +205,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } findRecord(store, schema, id, snapshot) { assert.step('findRecord'); - return resolve({ + return Promise.resolve({ data: { id, type: 'chapter', attributes: { title: `Chapter ${id}` } }, }); } @@ -256,13 +250,13 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( ); }); - test("When a hasMany relationship is accessed, the adapter's findMany method should not be called if all the records in the relationship are already loaded", function (assert) { - assert.expect(0); + test("When a hasMany relationship is accessed, the adapter's findMany method should not be called if all the records in the relationship are already loaded", async function (assert) { + assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let postData = { + const postData = { type: 'post', id: '1', relationships: { @@ -276,33 +270,37 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.ok(false, "The adapter's find method should not be called"); }; + adapter.shouldBackgroundReloadRecord = () => false; + adapter.findRecord = function (store, type, ids, snapshots) { - return { data: postData }; + assert.ok(false, "The adapter's find method should not be called"); }; - return run(() => { - store.push({ + store.push( + structuredClone({ data: postData, included: [ { type: 'comment', id: '1', + attributes: {}, }, ], - }); + }) + ); - return store.findRecord('post', 1).then((post) => { - return post.comments; - }); - }); + const post = await store.findRecord('post', '1'); + const comments = await post.comments; + + assert.strictEqual(comments.length, 1, 'The comments are correctly loaded'); }); test('hasMany + canonical vs currentState + destroyRecord ', async function (assert) { assert.expect(7); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let postData = { + const postData = { type: 'user', id: '1', attributes: { @@ -328,7 +326,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }; - let user = store.push({ + const user = store.push({ data: postData, included: [ { @@ -350,7 +348,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( return { data: { type: 'user', id: '2' } }; }; - let contacts = user.contacts; + const contacts = user.contacts; assert.deepEqual( contacts.map((c) => c.id), ['2', '3', '4'], @@ -379,9 +377,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.ok(!user.contacts.initialState || !user.contacts.initialState.find((model) => model.id === '2')); - run(() => { - contacts.push(store.createRecord('user', { id: '8' })); - }); + contacts.push(store.createRecord('user', { id: '8' })); assert.deepEqual( contacts.map((c) => c.id), @@ -394,9 +390,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( test('hasMany + canonical vs currentState + unloadRecord', function (assert) { assert.expect(6); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let postData = { + const postData = { type: 'user', id: '1', attributes: { @@ -422,7 +418,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }; - let user = store.push({ + const user = store.push({ data: postData, included: [ { @@ -439,7 +435,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, ], }); - let contacts = user.contacts; + const contacts = user.contacts; store.adapterFor('user').deleteRecord = function () { return { data: { type: 'user', id: '2' } }; @@ -451,11 +447,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( 'user should have expected contacts' ); - run(() => { - contacts.push(store.createRecord('user', { id: '5' })); - contacts.push(store.createRecord('user', { id: '6' })); - contacts.push(store.createRecord('user', { id: '7' })); - }); + contacts.push(store.createRecord('user', { id: '5' })); + contacts.push(store.createRecord('user', { id: '6' })); + contacts.push(store.createRecord('user', { id: '7' })); assert.deepEqual( contacts.map((c) => c.id), @@ -482,59 +476,71 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(contacts, user.contacts); }); - test('adapter.findMany only gets unique IDs even if duplicate IDs are present in the hasMany relationship', function (assert) { - assert.expect(2); + deprecatedTest( + 'adapter.findMany only gets unique IDs even if duplicate IDs are present in the hasMany relationship', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0', + count: 2, + }, + async function (assert) { + assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let Chapter = store.modelFor('chapter'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const Chapter = store.modelFor('chapter'); - let bookData = { - type: 'book', - id: '1', - relationships: { - chapters: { - data: [ - { type: 'chapter', id: '2' }, - { type: 'chapter', id: '3' }, - { type: 'chapter', id: '3' }, - ], + const bookData = { + type: 'book', + id: '1', + relationships: { + chapters: { + data: [ + { type: 'chapter', id: '2' }, + { type: 'chapter', id: '3' }, + { type: 'chapter', id: '3' }, + ], + }, }, - }, - }; + }; - adapter.findMany = function (store, type, ids, snapshots) { - assert.strictEqual(type, Chapter, 'type passed to adapter.findMany is correct'); - assert.deepEqual(ids, ['2', '3'], 'ids passed to adapter.findMany are unique'); + adapter.findMany = function (store, type, ids, snapshots) { + assert.strictEqual(type, Chapter, 'type passed to adapter.findMany is correct'); + assert.deepEqual(ids, ['2', '3'], 'ids passed to adapter.findMany are unique'); - return resolve({ - data: [ - { id: '2', type: 'chapter', attributes: { title: 'Chapter One' } }, - { id: '3', type: 'chapter', attributes: { title: 'Chapter Two' } }, - ], - }); - }; + return Promise.resolve({ + data: [ + { id: '2', type: 'chapter', attributes: { title: 'Chapter One' } }, + { id: '3', type: 'chapter', attributes: { title: 'Chapter Two' } }, + ], + }); + }; - adapter.findRecord = function (store, type, ids, snapshots) { - return { data: bookData }; - }; + adapter.findRecord = function (store, type, ids, snapshots) { + return structuredClone({ data: bookData }); + }; - return run(() => { - store.push({ - data: bookData, - }); + store.push( + structuredClone({ + data: bookData, + }) + ); - return store.findRecord('book', 1).then((book) => { - return book.chapters; - }); - }); - }); + const book = await store.findRecord('book', '1'); + const chapters = await book.chapters; + + assert.deepEqual( + chapters.map((c) => c.title), + ['Chapter One', 'Chapter Two'] + ); + } + ); // This tests the case where a serializer materializes a has-many // relationship as an identifier that it can fetch lazily. The most // common use case of this is to provide a URL to a collection that // is loaded later. - test("A serializer can materialize a hasMany as an opaque token that can be lazily fetched via the adapter's findHasMany hook", function (assert) { + test("A serializer can materialize a hasMany as an opaque token that can be lazily fetched via the adapter's findHasMany hook", async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: true, inverse: 'message' }) comments; @@ -547,8 +553,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); // When the store asks the adapter for the record with ID 1, // provide some fake data. @@ -556,7 +562,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(type, Post, 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -580,7 +586,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); assert.strictEqual(relationship.type, 'comment', 'relationship was passed correctly'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -588,18 +594,16 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - return run(() => { - return store - .findRecord('post', 1) - .then((post) => { - return post.comments; - }) - .then((comments) => { - assert.true(comments.isLoaded, 'comments are loaded'); - assert.strictEqual(comments.length, 2, 'comments have 2 length'); - assert.strictEqual(comments.at(0).body, 'First', 'comment loaded successfully'); - }); - }); + await store + .findRecord('post', 1) + .then((post) => { + return post.comments; + }) + .then((comments) => { + assert.true(comments.isLoaded, 'comments are loaded'); + assert.strictEqual(comments.length, 2, 'comments have 2 length'); + assert.strictEqual(comments.at(0).body, 'First', 'comment loaded successfully'); + }); }); test('Accessing a hasMany backed by a link multiple times triggers only one request', async function (assert) { @@ -616,10 +620,10 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -637,9 +641,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( adapter.findHasMany = function (store, snapshot, link, relationship) { count++; assert.strictEqual(count, 1, 'findHasMany has only been called once'); - return new EmberPromise((resolve, reject) => { + return new Promise((resolve, reject) => { setTimeout(() => { - let value = { + const value = { data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -650,9 +654,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - let promise1, promise2; - - promise1 = post.comments; + const promise1 = post.comments; //Invalidate the post.comments CP store.push({ data: { @@ -665,13 +667,13 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }, }, }); - promise2 = post.comments; + const promise2 = post.comments; - await all([promise1, promise2]); + await Promise.all([promise1, promise2]); assert.strictEqual(promise1.promise, promise2.promise, 'Same promise is returned both times'); }); - test('A hasMany backed by a link remains a promise after a record has been added to it', function (assert) { + test('A hasMany backed by a link remains a promise after a record has been added to it', async function (assert) { assert.expect(1); class Post extends Model { @attr title; @@ -686,11 +688,11 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -698,46 +700,40 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - let post; - run(() => { + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: '/posts/1/comments', + }, + }, + }, + }, + }); + + await post.comments.then(() => { store.push({ data: { - type: 'post', - id: '1', + type: 'comment', + id: '3', relationships: { - comments: { - links: { - related: '/posts/1/comments', - }, + message: { + data: { type: 'post', id: '1' }, }, }, }, }); - post = store.peekRecord('post', 1); - }); - return run(() => { return post.comments.then(() => { - store.push({ - data: { - type: 'comment', - id: '3', - relationships: { - message: { - data: { type: 'post', id: '1' }, - }, - }, - }, - }); - - return post.comments.then(() => { - assert.ok(true, 'Promise was called'); - }); + assert.ok(true, 'Promise was called'); }); }); }); - test('A hasMany updated link should not remove new children', function (assert) { + test('A hasMany updated link should not remove new children', async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: true, inverse: 'message' }) comments; @@ -749,15 +745,15 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, link, relationship) { - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); }; adapter.createRecord = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -770,24 +766,22 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - return run(() => { - let post = store.createRecord('post', {}); - store.createRecord('comment', { message: post }); + const post = store.createRecord('post', {}); + store.createRecord('comment', { message: post }); - return post.comments - .then((comments) => { - assert.strictEqual(comments.length, 1, 'initially we have one comment'); + await post.comments + .then((comments) => { + assert.strictEqual(comments.length, 1, 'initially we have one comment'); - return post.save(); - }) - .then(() => post.comments) - .then((comments) => { - assert.strictEqual(comments.length, 1, 'after saving, we still have one comment'); - }); - }); + return post.save(); + }) + .then(() => post.comments) + .then((comments) => { + assert.strictEqual(comments.length, 1, 'after saving, we still have one comment'); + }); }); - test('A hasMany updated link should not remove new children when the parent record has children already', function (assert) { + test('A hasMany updated link should not remove new children when the parent record has children already', async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: true, inverse: 'message' }) comments; @@ -800,17 +794,17 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: [{ id: '5', type: 'comment', attributes: { body: 'hello' } }], }); }; adapter.createRecord = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -823,23 +817,21 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - return run(() => { - let post = store.createRecord('post', {}); - store.createRecord('comment', { message: post }); + const post = store.createRecord('post', {}); + store.createRecord('comment', { message: post }); - return post.comments - .then((comments) => { - assert.strictEqual(comments.length, 1); - return post.save(); - }) - .then(() => post.comments) - .then((comments) => { - assert.strictEqual(comments.length, 2); - }); - }); + await post.comments + .then((comments) => { + assert.strictEqual(comments.length, 1); + return post.save(); + }) + .then(() => post.comments) + .then((comments) => { + assert.strictEqual(comments.length, 2); + }); }); - test("A hasMany relationship doesn't contain duplicate children, after the canonical state of the relationship is updated via store#push", function (assert) { + test("A hasMany relationship doesn't contain duplicate children, after the canonical state of the relationship is updated via store#push", async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: true, inverse: 'message' }) comments; @@ -852,58 +844,56 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, snapshot, link, relationship) { - return resolve({ data: { id: '1', type: 'post' } }); + return Promise.resolve({ data: { id: '1', type: 'post' } }); }; - return run(() => { - let post = store.createRecord('post', {}); + const post = store.createRecord('post', {}); - // create a new comment with id 'local', which is in the 'comments' - // relationship of post - let localComment = store.createRecord('comment', { id: 'local', message: post }); + // create a new comment with id 'local', which is in the 'comments' + // relationship of post + const localComment = store.createRecord('comment', { id: 'local', message: post }); - return post.comments - .then((comments) => { - assert.strictEqual(comments.length, 1); - assert.true(localComment.isNew); + await post.comments + .then((comments) => { + assert.strictEqual(comments.length, 1); + assert.true(localComment.isNew); - return post.save(); - }) - .then(() => { - // Now the post is saved but the locally created comment with the id - // 'local' is still in the created state since it hasn't been saved - // yet. - // - // As next we are pushing the post into the store again, having the - // locally created comment in the 'comments' relationship. By this the - // canonical state of the relationship is defined to consist of one - // comment: the one with id 'local'. - // - // This setup is needed to demonstrate the bug which has been fixed - // in #4154, where the locally created comment was duplicated in the - // comment relationship. - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [{ id: 'local', type: 'comment' }], - }, + return post.save(); + }) + .then(() => { + // Now the post is saved but the locally created comment with the id + // 'local' is still in the created state since it hasn't been saved + // yet. + // + // As next we are pushing the post into the store again, having the + // locally created comment in the 'comments' relationship. By this the + // canonical state of the relationship is defined to consist of one + // comment: the one with id 'local'. + // + // This setup is needed to demonstrate the bug which has been fixed + // in #4154, where the locally created comment was duplicated in the + // comment relationship. + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [{ id: 'local', type: 'comment' }], }, }, - }); - }) - .then(() => post.comments) - .then((comments) => { - assert.strictEqual(comments.length, 1); - assert.true(localComment.isNew); + }, }); - }); + }) + .then(() => post.comments) + .then((comments) => { + assert.strictEqual(comments.length, 1); + assert.true(localComment.isNew); + }); }); test('A hasMany relationship can be reloaded if it was fetched via a link', async function (assert) { @@ -919,14 +909,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(type, Post, 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -941,10 +931,10 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( adapter.findHasMany = function (store, snapshot, link, relationship) { assert.strictEqual(relationship.type, 'comment', 'findHasMany relationship type was Comment'); - assert.strictEqual(relationship.key, 'comments', 'findHasMany relationship key was comments'); + assert.strictEqual(relationship.name, 'comments', 'findHasMany relationship key was comments'); assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -952,17 +942,17 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }; - let post = await store.findRecord('post', 1); - let comments = await post.comments; + const post = await store.findRecord('post', 1); + const comments = await post.comments; assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); adapter.findHasMany = function (store, snapshot, link, relationship) { assert.strictEqual(relationship.type, 'comment', 'findHasMany relationship type was Comment'); - assert.strictEqual(relationship.key, 'comments', 'findHasMany relationship key was comments'); + assert.strictEqual(relationship.name, 'comments', 'findHasMany relationship key was comments'); assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -977,14 +967,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); test('A sync hasMany relationship can be reloaded if it was fetched via ids', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(type, store.modelFor('post'), 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1022,12 +1012,12 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( await store .findRecord('post', '1') .then(function (post) { - let comments = post.comments; + const comments = post.comments; assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have a length of 2'); adapter.findMany = function (store, type, ids, snapshots) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1056,14 +1046,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(type, Post, 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1080,7 +1070,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }; adapter.findMany = function (store, type, ids, snapshots) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1098,7 +1088,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(comments.length, 2, 'comments have 2 length'); adapter.findMany = function (store, type, ids, snapshots) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1127,11 +1117,11 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function () { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1148,9 +1138,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( adapter.findHasMany = function () { loadingCount++; if (loadingCount % 2 === 1) { - return reject({ data: null }); + return Promise.reject({ data: null }); } else { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1159,8 +1149,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } }; - let post = await store.findRecord('post', '1'); - let commentsPromiseArray = post.comments; + const post = await store.findRecord('post', '1'); + const commentsPromiseArray = post.comments; let manyArray; try { @@ -1182,7 +1172,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.true(manyArray.isLoaded, 'the second reload failed, comments are still loaded though'); - let reloadedManyArray = await manyArray.reload(); + const reloadedManyArray = await manyArray.reload(); assert.true(reloadedManyArray.isLoaded, 'the third reload worked, comments are loaded again'); assert.strictEqual(reloadedManyArray, manyArray, 'the many array stays the same'); @@ -1203,14 +1193,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id) { assert.strictEqual(type, Post, 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1226,7 +1216,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( adapter.findHasMany = function (store, record, link, relationship) { assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1256,13 +1246,13 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let done = assert.async(); + const done = assert.async(); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1278,7 +1268,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let count = 0; adapter.findHasMany = function (store, record, link, relationship) { count++; - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1287,11 +1277,11 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }; await store.findRecord('post', '1').then(function (post) { post.comments.then(function (comments) { - all([comments.reload(), comments.reload(), comments.reload()]).then(function (comments) { + Promise.all([comments.reload(), comments.reload(), comments.reload()]).then(function (comments) { assert.strictEqual( count, 2, - 'One request for the original access and only one request for the mulitple reloads' + 'One request for the original access and only one request for the multiple reloads' ); done(); }); @@ -1312,14 +1302,14 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(type, Post, 'find type was Post'); assert.strictEqual(id, '1', 'find id was 1'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1336,7 +1326,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }; adapter.findMany = function (store, type, ids, snapshots) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1367,13 +1357,13 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let done = assert.async(); + const done = assert.async(); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -1392,7 +1382,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( let count = 0; adapter.findMany = function (store, type, ids, snapshots) { count++; - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'FirstUpdated' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -1402,11 +1392,11 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( await store.findRecord('post', '1').then(function (post) { post.comments.then(function (comments) { - all([comments.reload(), comments.reload(), comments.reload()]).then(function (comments) { + Promise.all([comments.reload(), comments.reload(), comments.reload()]).then(function (comments) { assert.strictEqual( count, 2, - 'One request for the original access and only one request for the mulitple reloads' + 'One request for the original access and only one request for the multiple reloads' ); done(); }); @@ -1431,18 +1421,18 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, ], }); }; - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -1460,7 +1450,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.true(comments.isLoaded, 'comments are loaded'); assert.strictEqual(comments.length, 2, 'comments have 2 length'); - let newComment = post.comments.createRecord({ body: 'Third' }); + 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'); }); @@ -1481,21 +1471,21 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, link, relationship) { assert.strictEqual(relationship.type, 'comment', 'relationship was passed correctly'); if (link === '/first') { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, ], }); } else if (link === '/second') { - return resolve({ + return Promise.resolve({ data: [ { id: '3', type: 'comment', attributes: { body: 'Third' } }, { id: '4', type: 'comment', attributes: { body: 'Fourth' } }, @@ -1504,7 +1494,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); } }; - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -1544,48 +1534,49 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( test("When a polymorphic hasMany relationship is accessed, the adapter's findMany method should not be called if all the records in the relationship are already loaded", async function (assert) { assert.expect(1); - let userData = { - type: 'user', - id: '1', - relationships: { - messages: { - data: [ - { type: 'post', id: '1' }, - { type: 'comment', id: '3' }, - ], - }, - }, - }; - - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findMany = function (store, type, ids, snapshots) { assert.ok(false, "The adapter's find method should not be called"); }; adapter.findRecord = function (store, type, ids, snapshots) { - return { data: userData }; + return { + data: { + type: 'user', + id: '1', + relationships: { + messages: { + data: [ + { type: 'post', id: '1' }, + { type: 'comment', id: '3' }, + ], + }, + }, + }, + }; }; store.push({ - data: userData, - included: [ + data: [ { type: 'post', id: '1', + attributes: {}, }, { type: 'comment', id: '3', + attributes: {}, }, ], }); - await store.findRecord('user', '1').then(function (user) { - let messages = user.messages; - assert.strictEqual(messages.length, 2, 'The messages are correctly loaded'); - }); + const user = await store.findRecord('user', '1'); + const messages = await user.messages; + + assert.strictEqual(messages.length, 2, 'The messages are correctly loaded'); }); test("When a polymorphic hasMany relationship is accessed, the store can call multiple adapters' findMany or find methods if the records are not loaded", async function (assert) { @@ -1596,15 +1587,15 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } this.owner.register('model:user', User); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = function (store, type, id, snapshot) { if (type === store.modelFor('post')) { - return resolve({ data: { id: '1', type: 'post' } }); + return Promise.resolve({ data: { id: '1', type: 'post' } }); } else if (type === store.modelFor('comment')) { - return resolve({ data: { id: '3', type: 'comment' } }); + return Promise.resolve({ data: { id: '3', type: 'comment' } }); } }; @@ -1636,10 +1627,10 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( test('polymorphic hasMany type-checks check the superclass', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let igor = store.createRecord('user', { name: 'Igor' }); - let comment = store.createRecord('comment', { + const igor = store.createRecord('user', { name: 'Igor' }); + const comment = store.createRecord('comment', { body: 'Well I thought the title was fine', }); @@ -1667,8 +1658,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:user', User); this.owner.register('model:contact', Contact); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function () { return { @@ -1709,8 +1700,7 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( 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 }, - function (assert) { - assert.expect(1); + async function (assert) { class User extends Model { @attr name; @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; @@ -1718,8 +1708,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } this.owner.register('model:user', User); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, ids, snapshots) { return { @@ -1735,43 +1725,34 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }; }; - run(function () { - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - contacts: { - data: [{ type: 'contact', id: '1' }], - }, + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], }, }, - included: [ - { - type: 'contact', - id: '1', - }, - ], - }); - }); - run(function () { - store - .findRecord('user', 1) - .then(function (user) { - return user.contacts; - }) - .then(function (contacts) { - assert.strictEqual(contacts.length, 1, 'The contacts relationship is correctly set up'); - }); + }, + 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 }, - function (assert) { - assert.expect(1); + async function (assert) { class User extends Model { @attr name; @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; @@ -1779,49 +1760,41 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } this.owner.register('model:user', User); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + 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' } }; }; - run(function () { - store.push({ - data: { - type: 'user', - id: '1', - relationships: { - contacts: { - data: [ - { type: 'email', id: '1' }, - { type: 'phone', id: '2' }, - ], - }, + 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', - }, - ], - }); - }); - run(function () { - store - .findRecord('user', 1) - .then(function (user) { - return user.contacts; - }) - .then(function (contacts) { - assert.strictEqual(contacts.length, 2, 'The contacts relationship is correctly set up'); - }); + }, + 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'); } ); @@ -1888,10 +1861,10 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( this.owner.register('model:phone', Phone); this.owner.register('model:post', Post); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let email = store.createRecord('email'); - let post = store.createRecord('post', { + const email = store.createRecord('email'); + const post = store.createRecord('post', { contact: email, }); @@ -1941,8 +1914,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual( e.message, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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." - : "Assertion Failed: 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'`.", + ? "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' ); } @@ -2018,8 +1991,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( user.messages.push(anotherUser); }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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." - : `Assertion Failed: 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: + ? "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: \`\`\` { @@ -2152,8 +2125,8 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( user.messages.push(anotherUser); }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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." - : `Assertion Failed: No 'user' field exists on 'user'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for user should include: + ? "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: \`\`\` { @@ -2309,7 +2282,7 @@ If using this relationship in a polymorphic manner is desired, the relationships } catch (e) { assert.strictEqual( e.message, - `Assertion Failed: The '.messages' relationship cannot be used polymorphically because '.user is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for 'message': + `The '.messages' relationship cannot be used polymorphically because '.user is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for 'message': \`\`\` { @@ -2337,8 +2310,8 @@ If using this relationship in a polymorphic manner is desired, the relationships test('A record can be removed from a polymorphic association', async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -2365,7 +2338,7 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(messages.length, 1, 'The user has 1 message'); - let removedObject = messages.pop(); + const removedObject = messages.pop(); assert.strictEqual(removedObject, comment, 'The message is correctly removed'); assert.strictEqual(messages.length, 0, 'The user does not have any messages'); @@ -2375,11 +2348,11 @@ If using this relationship in a polymorphic manner is desired, the relationships test('When a record is created on the client, its hasMany arrays should be in a loaded state', async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post'); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post'); assert.ok(post.isLoaded, 'The post should have isLoaded flag'); - let comments = post.comments; + const comments = post.comments; await comments; assert.strictEqual(comments.length, 0, 'The comments should be an empty array'); @@ -2401,8 +2374,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post'); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post'); assert.ok(post.isLoaded, 'The post should have isLoaded flag'); @@ -2415,35 +2388,33 @@ If using this relationship in a polymorphic manner is desired, the relationships test('we can set records SYNC HM relationship', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post'); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post'); - run(function () { - store.push({ - data: [ - { - type: 'comment', - id: '1', - attributes: { - body: 'First', - }, + store.push({ + data: [ + { + type: 'comment', + id: '1', + attributes: { + body: 'First', }, - { - type: 'comment', - id: '2', - attributes: { - body: 'Second', - }, + }, + { + type: 'comment', + id: '2', + attributes: { + body: 'Second', }, - ], - }); - post.set('comments', store.peekAll('comment').slice()); + }, + ], }); + post.set('comments', store.peekAll('comment').slice()); - assert.strictEqual(get(post, 'comments.length'), 2, 'we can set HM relationship'); + assert.strictEqual(post.comments.length, 2, 'we can set HM relationship'); }); - test('We can set records ASYNC HM relationship', function (assert) { + test('We can set records ASYNC HM relationship', async function (assert) { assert.expect(1); class Post extends Model { @attr title; @@ -2457,55 +2428,49 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post'); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post'); - run(function () { - store.push({ - data: [ - { - type: 'comment', - id: '1', - attributes: { - body: 'First', - }, + store.push({ + data: [ + { + type: 'comment', + id: '1', + attributes: { + body: 'First', }, - { - type: 'comment', - id: '2', - attributes: { - body: 'Second', - }, + }, + { + type: 'comment', + id: '2', + attributes: { + body: 'Second', }, - ], - }); - post.set('comments', store.peekAll('comment').slice()); + }, + ], }); + post.set('comments', store.peekAll('comment').slice()); - return post.comments.then((comments) => { + await post.comments.then((comments) => { assert.strictEqual(comments.length, 2, 'we can set async HM relationship'); }); }); - test('When a record is saved, its unsaved hasMany records should be kept', function (assert) { + test('When a record is saved, its unsaved hasMany records should be kept', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, type, snapshot) { - return resolve({ data: { id: '1', type: snapshot.modelName } }); + return Promise.resolve({ data: { id: '1', type: snapshot.modelName } }); }; - let post, comment; - return run(() => { - post = store.createRecord('post'); - comment = store.createRecord('comment'); - post.comments.push(comment); - return post.save(); - }).then(() => { - assert.strictEqual(get(post, 'comments.length'), 1, "The unsaved comment should be in the post's comments array"); - }); + const post = store.createRecord('post'); + const comment = store.createRecord('comment'); + post.comments.push(comment); + await post.save(); + assert.strictEqual(post.comments.length, 1, "The unsaved comment should be in the post's comments array"); }); test('dual non-async HM <-> BT', async function (assert) { @@ -2521,16 +2486,16 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function (store, type, snapshot) { - let serialized = snapshot.record.serialize(); + const serialized = snapshot.record.serialize(); serialized.data.id = 2; - return resolve(serialized); + return Promise.resolve(serialized); }; - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -2541,7 +2506,7 @@ If using this relationship in a polymorphic manner is desired, the relationships }, }, }); - let firstComment = store.push({ + const firstComment = store.push({ data: { type: 'comment', id: '1', @@ -2556,9 +2521,9 @@ If using this relationship in a polymorphic manner is desired, the relationships const comment = store.createRecord('comment', { post }); await comment.save(); - let commentPost = comment.post; - let postComments = comment.post.comments; - let postCommentsLength = comment.get('post.comments.length'); + const commentPost = comment.post; + const postComments = comment.post.comments; + const postCommentsLength = comment.get('post.comments.length'); assert.deepEqual(post, commentPost, 'expect the new comments post, to be the correct post'); assert.ok(postComments, 'comments should exist'); @@ -2581,15 +2546,15 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let findManyCalls = 0; let findRecordCalls = 0; adapter.findMany = function (store, type, ids, snapshots) { assert.ok(true, `findMany called ${++findManyCalls}x`); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'first' } }, { id: '2', type: 'comment', attributes: { body: 'second' } }, @@ -2600,10 +2565,10 @@ If using this relationship in a polymorphic manner is desired, the relationships adapter.findRecord = function (store, type, id, snapshot) { assert.ok(true, `findRecord called ${++findRecordCalls}x`); - return resolve({ data: { id: '3', type: 'comment', attributes: { body: 'third' } } }); + return Promise.resolve({ data: { id: '3', type: 'comment', attributes: { body: 'third' } } }); }; - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -2618,7 +2583,7 @@ If using this relationship in a polymorphic manner is desired, the relationships }, }); - let fetchedComments = await post.comments; + const fetchedComments = await post.comments; assert.strictEqual(fetchedComments.length, 2, 'comments fetched successfully'); assert.strictEqual(fetchedComments.at(0).body, 'first', 'first comment loaded successfully'); @@ -2639,16 +2604,16 @@ If using this relationship in a polymorphic manner is desired, the relationships }, }); - let newlyFetchedComments = await post.comments; + const newlyFetchedComments = await post.comments; assert.strictEqual(newlyFetchedComments.length, 3, 'all three comments fetched successfully'); assert.strictEqual(newlyFetchedComments.at(2).body, 'third', 'third comment loaded successfully'); }); testInDebug('A sync hasMany errors out if there are unloaded records in it', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let post = store.push({ + const post = store.push({ data: { type: 'post', id: '1', @@ -2675,8 +2640,8 @@ If using this relationship in a polymorphic manner is desired, the relationships }); testInDebug('An async hasMany does not fetch with a model created with no options', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function () { assert.ok(false, 'no request should be made'); }; @@ -2697,8 +2662,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let comment = store.createRecord('comment'); - let post = store.push({ + const comment = store.createRecord('comment'); + const post = store.push({ data: { type: 'post', id: '1', @@ -2710,72 +2675,53 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.ok(post.comments.length, 1, 'expected length for comments'); }); - test('After removing and unloading a record, a hasMany relationship should still be valid', function (assert) { - let store = this.owner.lookup('service:store'); + test('After removing and unloading a record, a hasMany relationship should still be valid', async function (assert) { + const store = this.owner.lookup('service:store'); - const post = run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [{ type: 'comment', id: '1' }], - }, + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [{ type: 'comment', id: '1' }], }, }, - included: [{ type: 'comment', id: '1' }], - }); - const post = store.peekRecord('post', 1); - const comments = post.comments; - const comment = comments.at(0); - comments.splice(0, 1); - store.unloadRecord(comment); - assert.strictEqual(comments.length, 0); - return post; + }, + included: [{ type: 'comment', id: '1' }], }); - - // Explicitly re-get comments - assert.strictEqual(run(post, 'get', 'comments.length'), 0); + const comments = post.comments; + const comment = comments.at(0); + comments.splice(0, 1); + store.unloadRecord(comment); + assert.strictEqual(comments.length, 0); + + const comments2 = await post.comments; + assert.strictEqual(comments2.length, 0); }); - test('If reordered hasMany data has been pushed to the store, the many array reflects the ordering change - sync', function (assert) { - let store = this.owner.lookup('service:store'); - - let comment1, comment2, comment3, comment4; - let post; - - run(() => { - store.push({ - data: [ - { - type: 'comment', - id: '1', - }, - { - type: 'comment', - id: '2', - }, - { - type: 'comment', - id: '3', - }, - { - type: 'comment', - id: '4', - }, - ], - }); - - comment1 = store.peekRecord('comment', 1); - comment2 = store.peekRecord('comment', 2); - comment3 = store.peekRecord('comment', 3); - comment4 = store.peekRecord('comment', 4); - }); + test('If reordered hasMany data has been pushed to the store, the many array reflects the ordering change - sync', async function (assert) { + const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: { + const [comment1, comment2, comment3, comment4, post] = store.push({ + data: [ + { + type: 'comment', + id: '1', + }, + { + type: 'comment', + id: '2', + }, + { + type: 'comment', + id: '3', + }, + { + type: 'comment', + id: '4', + }, + { type: 'post', id: '1', relationships: { @@ -2787,108 +2733,104 @@ If using this relationship in a polymorphic manner is desired, the relationships }, }, }, - }); - post = store.peekRecord('post', 1); - - assert.deepEqual(post.comments.slice(), [comment1, comment2], 'Initial ordering is correct'); + ], }); - run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [ - { type: 'comment', id: '2' }, - { type: 'comment', id: '1' }, - ], - }, + assert.arrayStrictEquals(post.comments.slice(), [comment1, comment2], 'Initial ordering is correct'); + + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [ + { type: 'comment', id: '2' }, + { type: 'comment', id: '1' }, + ], }, }, - }); + }, }); - assert.deepEqual(post.comments.slice(), [comment2, comment1], 'Updated ordering is correct'); + assert.arrayStrictEquals(post.comments.slice(), [comment2, comment1], 'Updated ordering is correct'); - run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [{ type: 'comment', id: '2' }], - }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [{ type: 'comment', id: '2' }], }, }, - }); + }, }); - assert.deepEqual(post.comments.slice(), [comment2], 'Updated ordering is correct'); + assert.arrayStrictEquals(post.comments.slice(), [comment2], 'Updated ordering is correct'); - run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [ - { type: 'comment', id: '1' }, - { type: 'comment', id: '2' }, - { type: 'comment', id: '3' }, - { type: 'comment', id: '4' }, - ], - }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [ + { type: 'comment', id: '1' }, + { type: 'comment', id: '2' }, + { type: 'comment', id: '3' }, + { type: 'comment', id: '4' }, + ], }, }, - }); + }, }); - assert.deepEqual(post.comments.slice(), [comment1, comment2, comment3, comment4], 'Updated ordering is correct'); + assert.arrayStrictEquals( + post.comments.slice(), + [comment1, comment2, comment3, comment4], + 'Updated ordering is correct' + ); - run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [ - { type: 'comment', id: '4' }, - { type: 'comment', id: '3' }, - ], - }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [ + { type: 'comment', id: '4' }, + { type: 'comment', id: '3' }, + ], }, }, - }); + }, }); - assert.deepEqual(post.comments.slice(), [comment4, comment3], 'Updated ordering is correct'); + assert.arrayStrictEquals(post.comments.slice(), [comment4, comment3], 'Updated ordering is correct'); - run(() => { - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [ - { type: 'comment', id: '4' }, - { type: 'comment', id: '2' }, - { type: 'comment', id: '3' }, - { type: 'comment', id: '1' }, - ], - }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [ + { type: 'comment', id: '4' }, + { type: 'comment', id: '2' }, + { type: 'comment', id: '3' }, + { type: 'comment', id: '1' }, + ], }, }, - }); + }, }); - - assert.deepEqual(post.comments.slice(), [comment4, comment2, comment3, comment1], 'Updated ordering is correct'); + assert.arrayStrictEquals( + post.comments.slice(), + [comment4, comment2, comment3, comment1], + 'Updated ordering is correct' + ); }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the hasMany side has been deleted - async', async function (assert) { - let store = this.owner.lookup('service:store'); - let book = store.push({ + const store = this.owner.lookup('service:store'); + const book = store.push({ data: { type: 'book', id: '1', @@ -2911,7 +2853,7 @@ If using this relationship in a polymorphic manner is desired, the relationships }, ], }); - let chapter = store.peekRecord('chapter', '2'); + const chapter = store.peekRecord('chapter', '2'); chapter.deleteRecord(); chapter.rollbackAttributes(); @@ -2921,8 +2863,8 @@ If using this relationship in a polymorphic manner is desired, the relationships }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the hasMany side has been deleted - sync', async function (assert) { - let store = this.owner.lookup('service:store'); - let chapter = store.push({ + const store = this.owner.lookup('service:store'); + const chapter = store.push({ data: { type: 'chapter', id: '1', @@ -2945,7 +2887,7 @@ If using this relationship in a polymorphic manner is desired, the relationships }, ], }); - let page = store.peekRecord('page', '2'); + const page = store.peekRecord('page', '2'); page.deleteRecord(); page.rollbackAttributes(); @@ -2953,97 +2895,81 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(chapter.pages.at(0), page, 'Chapter has a page after rollback attributes'); }); - test('Rollbacking attributes for deleted record restores implicit relationship correctly when the belongsTo side has been deleted - async', function (assert) { + test('Rollbacking attributes for deleted record restores implicit relationship correctly when the belongsTo side has been deleted - async', async function (assert) { class Page extends Model { @attr number; @belongsTo('chapter', { async: true, inverse: 'pages' }) chapter; } this.owner.register('model:page', Page); - let store = this.owner.lookup('service:store'); - - let chapter, page; + const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: { - type: 'chapter', - id: '2', + store.push({ + data: { + type: 'chapter', + id: '2', + attributes: { + title: 'Sailing the Seven Seas', + }, + }, + included: [ + { + type: 'page', + id: '3', attributes: { - title: 'Sailing the Seven Seas', + number: 1, }, - }, - included: [ - { - type: 'page', - id: '3', - attributes: { - number: 1, - }, - relationships: { - chapter: { - data: { type: 'chapter', id: '2' }, - }, + relationships: { + chapter: { + data: { type: 'chapter', id: '2' }, }, }, - ], - }); - chapter = store.peekRecord('chapter', 2); - page = store.peekRecord('page', 3); - }); - - run(() => { - chapter.deleteRecord(); - chapter.rollbackAttributes(); + }, + ], }); + const chapter = store.peekRecord('chapter', '2'); + const page = store.peekRecord('page', '3'); - return run(() => { - return page.chapter.then((fetchedChapter) => { - assert.strictEqual(fetchedChapter, chapter, 'Page has a chapter after rollback attributes'); - }); + chapter.deleteRecord(); + chapter.rollbackAttributes(); + await page.chapter.then((fetchedChapter) => { + assert.strictEqual(fetchedChapter, chapter, 'Page has a chapter after rollback attributes'); }); }); test('Rollbacking attributes for deleted record restores implicit relationship correctly when the belongsTo side has been deleted - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let chapter, page; - run(() => { - store.push({ - data: { - type: 'chapter', - id: '2', + store.push({ + data: { + type: 'chapter', + id: '2', + attributes: { + title: 'Sailing the Seven Seas', + }, + }, + included: [ + { + type: 'page', + id: '3', attributes: { - title: 'Sailing the Seven Seas', + number: 1, }, - }, - included: [ - { - type: 'page', - id: '3', - attributes: { - number: 1, - }, - relationships: { - chapter: { - data: { type: 'chapter', id: '2' }, - }, + relationships: { + chapter: { + data: { type: 'chapter', id: '2' }, }, }, - ], - }); - chapter = store.peekRecord('chapter', 2); - page = store.peekRecord('page', 3); + }, + ], }); + const chapter = store.peekRecord('chapter', '2'); + const page = store.peekRecord('page', '3'); - run(() => { - chapter.deleteRecord(); - chapter.rollbackAttributes(); - }); + chapter.deleteRecord(); + chapter.rollbackAttributes(); - run(() => { - assert.strictEqual(page.chapter, chapter, 'Page has a chapter after rollback attributes'); - }); + 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) { @@ -3074,7 +3000,7 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const [post] = store.push({ data: [ @@ -3144,7 +3070,7 @@ If using this relationship in a polymorphic manner is desired, the relationships ); }); - test('unloading a record with associated records does not prevent the store from tearing down', function (assert) { + test('unloading a record with associated records does not prevent the store from tearing down', async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: false, inverse: 'post' }) comments; @@ -3157,64 +3083,60 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let post; - run(() => { - store.push({ - data: [ - { - type: 'post', - id: '2', - attributes: { - title: 'Sailing the Seven Seas', - }, - relationships: { - comments: { - data: [ - { type: 'comment', id: '1' }, - { type: 'comment', id: '2' }, - ], - }, + store.push({ + data: [ + { + type: 'post', + id: '2', + attributes: { + title: 'Sailing the Seven Seas', + }, + relationships: { + comments: { + data: [ + { type: 'comment', id: '1' }, + { type: 'comment', id: '2' }, + ], }, }, - { - type: 'comment', - id: '1', - relationships: { - post: { - data: { type: 'post', id: '2' }, - }, + }, + { + type: 'comment', + id: '1', + relationships: { + post: { + data: { type: 'post', id: '2' }, }, }, - { - type: 'comment', - id: '2', - relationships: { - post: { - data: { type: 'post', id: '2' }, - }, + }, + { + type: 'comment', + id: '2', + relationships: { + post: { + data: { type: 'post', id: '2' }, }, }, - ], - }); - post = store.peekRecord('post', 2); - - // This line triggers the original bug that gets manifested - // in teardown for apps, e.g. store.destroy that is caused by - // App.destroy(). - // Relationship#clear uses Ember.Set#forEach, which does incorrect - // iteration when the set is being mutated (in our case, the index gets off - // because records are being removed) - store.unloadRecord(post); + }, + ], }); + const post = store.peekRecord('post', '2'); + + // This line triggers the original bug that gets manifested + // in teardown for apps, e.g. store.destroy that is caused by + // App.destroy(). + // Relationship#clear uses Ember.Set#forEach, which does incorrect + // iteration when the set is being mutated (in our case, the index gets off + // because records are being removed) + store.unloadRecord(post); try { - run(() => { - store.destroy(); - }); + store.destroy(); + await settled(); assert.ok(true, 'store destroyed correctly'); - } catch (error) { + } catch { assert.ok(false, 'store prevented from being destroyed'); } }); @@ -3241,13 +3163,13 @@ If using this relationship in a polymorphic manner is desired, the relationships 'adapter:comment', RESTAdapter.extend({ deleteRecord(record) { - return resolve(); + return Promise.resolve(); }, updateRecord(record) { - return resolve(); + return Promise.resolve(); }, createRecord() { - return resolve({ comments: { id: commentId++ } }); + return Promise.resolve({ comments: { id: commentId++ } }); }, }) ); @@ -3258,7 +3180,7 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -3292,7 +3214,7 @@ If using this relationship in a polymorphic manner is desired, the relationships const post = await store.findRecord('post', '1'); let commentsPromiseArray = post.comments; - let comments = await commentsPromiseArray; + const comments = await commentsPromiseArray; assert.strictEqual(commentsPromiseArray.length, 3, 'Initial comments count'); // Add comment #4 @@ -3318,7 +3240,7 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(commentsPromiseArray.length, 4, 'Comments count after second add'); }); - test('hasMany hasAnyRelationshipData async loaded', function (assert) { + test('hasMany hasAnyRelationshipData async loaded', async function (assert) { assert.expect(1); class Chapter extends Model { @attr title; @@ -3326,11 +3248,11 @@ If using this relationship in a polymorphic manner is desired, the relationships } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'chapter', @@ -3347,22 +3269,20 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - return store.findRecord('chapter', 1).then((chapter) => { - let relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.true(relationship.state.hasReceivedData, 'relationship has data'); - }); + await store.findRecord('chapter', '1').then((chapter) => { + const relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); - test('hasMany hasAnyRelationshipData sync loaded', function (assert) { + test('hasMany hasAnyRelationshipData sync loaded', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'chapter', @@ -3379,15 +3299,13 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - return store.findRecord('chapter', 1).then((chapter) => { - let relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.true(relationship.state.hasReceivedData, 'relationship has data'); - }); + await store.findRecord('chapter', '1').then((chapter) => { + const relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.true(relationship.state.hasReceivedData, 'relationship has data'); }); }); - test('hasMany hasAnyRelationshipData async not loaded', function (assert) { + test('hasMany hasAnyRelationshipData async not loaded', async function (assert) { assert.expect(1); class Chapter extends Model { @attr title; @@ -3395,11 +3313,11 @@ If using this relationship in a polymorphic manner is desired, the relationships } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'chapter', @@ -3413,22 +3331,20 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - return store.findRecord('chapter', 1).then((chapter) => { - let relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); - }); + await store.findRecord('chapter', '1').then((chapter) => { + const relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); - test('hasMany hasAnyRelationshipData sync not loaded', function (assert) { + test('hasMany hasAnyRelationshipData sync not loaded', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'chapter', @@ -3437,11 +3353,9 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - return store.findRecord('chapter', 1).then((chapter) => { - let relationship = getRelationshipStateForRecord(chapter, 'pages'); - assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); - }); + await store.findRecord('chapter', '1').then((chapter) => { + const relationship = getRelationshipStateForRecord(chapter, 'pages'); + assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); }); }); @@ -3453,9 +3367,9 @@ If using this relationship in a polymorphic manner is desired, the relationships } this.owner.register('model:chapter', Chapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let chapter = store.createRecord('chapter', { title: 'The Story Begins' }); - let page = store.createRecord('page'); + const page = store.createRecord('page'); let relationship = getRelationshipStateForRecord(chapter, 'pages'); assert.false(relationship.state.hasReceivedData, 'relationship does not have data'); @@ -3472,7 +3386,7 @@ If using this relationship in a polymorphic manner is desired, the relationships test('hasMany hasAnyRelationshipData sync created', function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let chapter = store.createRecord('chapter', { title: 'The Story Begins' }); let relationship = getRelationshipStateForRecord(chapter, 'pages'); @@ -3488,76 +3402,59 @@ If using this relationship in a polymorphic manner is desired, the relationships }); test("Model's hasMany relationship should not be created during model creation", function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user; - run(() => { - store.push({ - data: { - type: 'user', - id: '1', - }, - }); - user = store.peekRecord('user', 1); - assert.notOk(hasRelationshipForRecord(user, 'messages'), 'Newly created record should not have relationships'); + const user = store.push({ + data: { + type: 'user', + id: '1', + }, }); + assert.notOk(hasRelationshipForRecord(user, 'messages'), 'Newly created record should not have relationships'); }); - test("Model's belongsTo relationship should be created during 'get' method", function (assert) { - let store = this.owner.lookup('service:store'); + test("Model's belongsTo relationship should be created during 'get' method", async function (assert) { + const store = this.owner.lookup('service:store'); - let user; - run(() => { - user = store.createRecord('user'); - user.messages; - assert.ok( - hasRelationshipForRecord(user, 'messages'), - 'Newly created record with relationships in params passed in its constructor should have relationships' - ); - }); + const user = store.createRecord('user'); + user.messages; + assert.ok( + hasRelationshipForRecord(user, 'messages'), + 'Newly created record with relationships in params passed in its constructor should have relationships' + ); }); test('metadata is accessible when pushed as a meta property for a relationship', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function () { - return resolve({}); + return Promise.resolve({}); }; - let book; - run(() => { - store.push({ - data: { - type: 'book', - id: '1', - attributes: { - title: 'Sailing the Seven Seas', - }, - relationships: { - chapters: { - meta: { - where: 'the lefkada sea', - }, - links: { - related: '/chapters', - }, + const book = store.push({ + data: { + type: 'book', + id: '1', + attributes: { + title: 'Sailing the Seven Seas', + }, + relationships: { + chapters: { + meta: { + where: 'the lefkada sea', + }, + links: { + related: '/chapters', }, }, }, - }); - book = store.peekRecord('book', 1); + }, }); - run(() => { - assert.strictEqual( - getRelationshipStateForRecord(book, 'chapters').meta.where, - 'the lefkada sea', - 'meta is there' - ); - }); + assert.strictEqual(getRelationshipStateForRecord(book, 'chapters').meta.where, 'the lefkada sea', 'meta is there'); }); test('metadata is accessible when return from a fetchLink', async function (assert) { @@ -3565,11 +3462,11 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('serializer:application', RESTSerializer); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function () { - return resolve({ + return Promise.resolve({ meta: { foo: 'bar', }, @@ -3595,20 +3492,20 @@ If using this relationship in a polymorphic manner is desired, the relationships }); const chapters = await book.chapters; - let meta = chapters.meta; + const meta = chapters.meta; assert.strictEqual(meta?.foo, 'bar', 'metadata is available'); }); - test('metadata should be reset between requests', function (assert) { + test('metadata should be reset between requests', async function (assert) { this.owner.register('serializer:application', RESTSerializer); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let counter = 0; adapter.findHasMany = function () { - let data = { + const data = { meta: { foo: 'bar', }, @@ -3623,62 +3520,56 @@ If using this relationship in a polymorphic manner is desired, the relationships counter++; - return resolve(data); + return Promise.resolve(data); }; - let book1, book2; - - run(() => { - store.push({ - data: [ - { - type: 'book', - id: '1', - attributes: { - title: 'Sailing the Seven Seas', - }, - relationships: { - chapters: { - links: { - related: 'chapters', - }, + store.push({ + data: [ + { + type: 'book', + id: '1', + attributes: { + title: 'Sailing the Seven Seas', + }, + relationships: { + chapters: { + links: { + related: 'chapters', }, }, }, - { - type: 'book', - id: '2', - attributes: { - title: 'Another book title', - }, - relationships: { - chapters: { - links: { - related: 'chapters', - }, + }, + { + type: 'book', + id: '2', + attributes: { + title: 'Another book title', + }, + relationships: { + chapters: { + links: { + related: 'chapters', }, }, }, - ], - }); - book1 = store.peekRecord('book', 1); - book2 = store.peekRecord('book', 2); + }, + ], }); + const book1 = store.peekRecord('book', '1'); + const book2 = store.peekRecord('book', '2'); - return run(() => { - return book1.chapters.then((chapters) => { - let meta = chapters.meta; - assert.strictEqual(get(meta, 'foo'), 'bar', 'metadata should available'); + await book1.chapters.then((chapters) => { + const meta = chapters.meta; + assert.strictEqual(meta.foo, 'bar', 'metadata should available'); - return book2.chapters.then((chapters) => { - let meta = chapters.meta; - assert.strictEqual(meta, null, 'metadata should not be available'); - }); + return book2.chapters.then((chapters) => { + const meta = chapters.meta; + assert.strictEqual(meta, null, 'metadata should not be available'); }); }); }); - test('Related link should be fetched when no relationship data is present', function (assert) { + test('Related link should be fetched when no relationship data is present', async function (assert) { assert.expect(3); class Post extends Model { @attr title; @@ -3692,8 +3583,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -3708,7 +3599,7 @@ If using this relationship in a polymorphic manner is desired, the relationships adapter.findHasMany = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'get-comments', 'url is correct'); assert.ok(true, "The adapter's findHasMany method should be called"); - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -3721,28 +3612,26 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'get-comments', - }, + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'get-comments', }, }, }, - }); + }, + }); - return post.comments.then((comments) => { - assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); - }); + await post.comments.then((comments) => { + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); - test('Related link should take precedence over relationship data when local record data is missing', function (assert) { + test('Related link should take precedence over relationship data when local record data is missing', async function (assert) { assert.expect(3); class Post extends Model { @attr title; @@ -3756,8 +3645,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -3772,7 +3661,7 @@ If using this relationship in a polymorphic manner is desired, the relationships adapter.findHasMany = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'get-comments', 'url is correct'); assert.ok(true, "The adapter's findHasMany method should be called"); - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -3785,29 +3674,27 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'get-comments', - }, - data: [{ type: 'comment', id: '1' }], + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'get-comments', }, + data: [{ type: 'comment', id: '1' }], }, }, - }); + }, + }); - return post.comments.then((comments) => { - assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); - }); + await post.comments.then((comments) => { + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); - test('Local relationship data should take precedence over related link when local record data is available', function (assert) { + test('Local relationship data should take precedence over related link when local record data is available', async function (assert) { assert.expect(1); class Post extends Model { @attr title; @@ -3821,8 +3708,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -3838,38 +3725,36 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.ok(false, "The adapter's findHasMany method should not be called"); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'get-comments', - }, - data: [{ type: 'comment', id: '1' }], + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'get-comments', }, + data: [{ type: 'comment', id: '1' }], }, }, - included: [ - { - id: '1', - type: 'comment', - attributes: { - body: 'This is comment', - }, + }, + included: [ + { + id: '1', + type: 'comment', + attributes: { + body: 'This is comment', }, - ], - }); + }, + ], + }); - return post.comments.then((comments) => { - assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); - }); + await post.comments.then((comments) => { + assert.strictEqual(comments.at(0).body, 'This is comment', 'comment body is correct'); }); }); - test('Related link should take precedence over local record data when relationship data is not initially available', function (assert) { + test('Related link should take precedence over local record data when relationship data is not initially available', async function (assert) { assert.expect(3); class Post extends Model { @attr title; @@ -3883,8 +3768,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -3899,7 +3784,7 @@ If using this relationship in a polymorphic manner is desired, the relationships adapter.findHasMany = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'get-comments', 'url is correct'); assert.ok(true, "The adapter's findHasMany method should be called"); - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -3912,45 +3797,43 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'get-comments', - }, + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'get-comments', }, }, }, - included: [ - { - id: '1', - type: 'comment', - attributes: { - body: 'This is comment', - }, - relationships: { - post: { - data: { - type: 'post', - id: '1', - }, + }, + included: [ + { + id: '1', + type: 'comment', + attributes: { + body: 'This is comment', + }, + relationships: { + post: { + data: { + type: 'post', + id: '1', }, }, }, - ], - }); + }, + ], + }); - return post.comments.then((comments) => { - assert.strictEqual(comments.at(0).body, 'This is comment fetched by link', 'comment body is correct'); - }); + await post.comments.then((comments) => { + assert.strictEqual(comments.at(0).body, 'This is comment fetched by link', 'comment body is correct'); }); }); - test('Updated related link should take precedence over relationship data and local record data', function (assert) { + test('Updated related link should take precedence over relationship data and local record data', async function (assert) { assert.expect(3); class Post extends Model { @attr title; @@ -3964,13 +3847,13 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, url, relationship) { assert.strictEqual(url, 'comments-updated-link', 'url is correct'); assert.ok(true, "The adapter's findHasMany method should be called"); - return resolve({ + return Promise.resolve({ data: [{ id: '1', type: 'comment', attributes: { body: 'This is updated comment' } }], }); }; @@ -3979,47 +3862,44 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.ok(false, "The adapter's findRecord method should not be called"); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'comments', - }, - data: [{ type: 'comment', id: '1' }], + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'comments', }, + data: [{ type: 'comment', id: '1' }], }, }, - }); + }, + }); - store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'comments-updated-link', - }, + store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'comments-updated-link', }, }, }, - }); + }, + }); - return post.comments.then((comments) => { - assert.strictEqual(comments.at(0).body, 'This is updated comment', 'comment body is correct'); - }); + await post.comments.then((comments) => { + assert.strictEqual(comments.at(0).body, 'This is updated comment', 'comment body is correct'); }); }); 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 }, - function (assert) { - assert.expect(1); + async function (assert) { class Post extends Model { @attr title; @hasMany('comment', { async: true, inverse: 'message' }) comments; @@ -4031,11 +3911,11 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, record, link, relationship) { - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -4043,27 +3923,24 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }; - return run(() => { - let post = store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - links: { - related: 'someLink', - }, + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'someLink', }, }, }, - }); - - let comments = post.comments; - comments.createRecord(); - return comments.then((comments) => { - assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); - }); + }, }); + + const commentsPromise = post.comments; + commentsPromise.createRecord(); + const comments = await commentsPromise; + assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); } ); @@ -4082,7 +3959,7 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:user', User); this.owner.register('model:post', Post); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -4112,9 +3989,9 @@ If using this relationship in a polymorphic manner is desired, the relationships ], }); - let user = store.peekRecord('user', 'user-1'); - let postsPromiseArray = user.posts; - let posts = await postsPromiseArray; + const user = store.peekRecord('user', 'user-1'); + const postsPromiseArray = user.posts; + const posts = await postsPromiseArray; store.adapterFor('post').deleteRecord = function () { // just acknowledge all deletes, but with a noop @@ -4153,7 +4030,7 @@ If using this relationship in a polymorphic manner is desired, the relationships }); test('unloading and reloading a record with hasMany relationship - #3084', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -4177,14 +4054,12 @@ If using this relationship in a polymorphic manner is desired, the relationships }); let user = store.peekRecord('user', 'user-1'); - let message = store.peekRecord('message', 'message-1'); + const message = store.peekRecord('message', 'message-1'); assert.strictEqual(user.messages.at(0).id, 'message-1'); assert.strictEqual(message.user.id, 'user-1'); - run(() => { - store.unloadRecord(user); - }); + store.unloadRecord(user); // The record is resurrected for some reason. store.push({ @@ -4211,10 +4086,8 @@ If using this relationship in a polymorphic manner is desired, the relationships }); test('deleted records should stay deleted', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let user; - let message; + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (store, type, id) { return null; @@ -4248,10 +4121,10 @@ If using this relationship in a polymorphic manner is desired, the relationships ], }); - user = store.peekRecord('user', 'user-1'); - message = store.peekRecord('message', 'message-1'); + const user = store.peekRecord('user', 'user-1'); + const message = store.peekRecord('message', 'message-1'); - assert.strictEqual(get(user, 'messages.length'), 2); + assert.strictEqual(user.messages.length, 2); await message.destroyRecord(); @@ -4272,41 +4145,37 @@ If using this relationship in a polymorphic manner is desired, the relationships }); assert.deepEqual( - get(user, 'messages').map((r) => r.id), + user.messages.map((r) => r.id), ['message-2', 'message-3'], 'user should have 2 message since 1 was deleted' ); }); - test("hasMany relationship with links doesn't trigger extra change notifications - #4942", function (assert) { - let store = this.owner.lookup('service:store'); + test("hasMany relationship with links doesn't trigger extra change notifications - #4942", async function (assert) { + const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: { - type: 'book', - id: '1', - relationships: { - chapters: { - data: [{ type: 'chapter', id: '1' }], - links: { related: '/book/1/chapters' }, - }, + store.push({ + data: { + type: 'book', + id: '1', + relationships: { + chapters: { + data: [{ type: 'chapter', id: '1' }], + links: { related: '/book/1/chapters' }, }, }, - included: [{ type: 'chapter', id: '1' }], - }); + }, + included: [{ type: 'chapter', id: '1' }], }); - let book = store.peekRecord('book', '1'); + const book = store.peekRecord('book', '1'); let count = 0; book.addObserver('chapters', () => { count++; }); - run(() => { - book.chapters; - }); + await book.chapters; assert.strictEqual(count, 0); }); @@ -4324,8 +4193,8 @@ If using this relationship in a polymorphic manner is desired, the relationships this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); const postID = '1'; @@ -4368,11 +4237,11 @@ If using this relationship in a polymorphic manner is desired, the relationships let hasManyCounter = 0; adapter.findHasMany = function (store, snapshot, link, relationship) { assert.strictEqual(relationship.type, 'comment', 'findHasMany relationship type was Comment'); - assert.strictEqual(relationship.key, 'comments', 'findHasMany relationship key was comments'); + assert.strictEqual(relationship.name, 'comments', 'findHasMany relationship key was comments'); assert.strictEqual(link, '/posts/1/comments', 'findHasMany link was /posts/1/comments'); hasManyCounter++; - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'comment', attributes: { body: 'First' } }, { id: '2', type: 'comment', attributes: { body: 'Second' } }, @@ -4394,51 +4263,60 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(commentsAgain.length, 2, 'comments have 2 length'); }); - test('Pushing a relationship with duplicate identifiers results in a single entry for the record in the relationship', async function (assert) { - class PhoneUser extends Model { - @hasMany('phone-number', { async: false, inverse: null }) - phoneNumbers; - @attr name; - } - class PhoneNumber extends Model { - @attr number; - } - const { owner } = this; + deprecatedTest( + 'Pushing a relationship with duplicate identifiers results in a single entry for the record in the relationship', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + until: '6.0', + count: 1, + refactor: true, // should assert + }, + async function (assert) { + class PhoneUser extends Model { + @hasMany('phone-number', { async: false, inverse: null }) + phoneNumbers; + @attr name; + } + class PhoneNumber extends Model { + @attr number; + } + const { owner } = this; - owner.register('model:phone-user', PhoneUser); - owner.register('model:phone-number', PhoneNumber); + owner.register('model:phone-user', PhoneUser); + owner.register('model:phone-number', PhoneNumber); - const store = owner.lookup('service:store'); + const store = owner.lookup('service:store'); - store.push({ - data: { - id: 'call-me-anytime', - type: 'phone-number', - attributes: { - number: '1-800-DATA', + store.push({ + data: { + id: 'call-me-anytime', + type: 'phone-number', + attributes: { + number: '1-800-DATA', + }, }, - }, - }); + }); - const person = store.push({ - data: { - id: '1', - type: 'phone-user', - attributes: {}, - relationships: { - phoneNumbers: { - data: [ - { type: 'phone-number', id: 'call-me-anytime' }, - { type: 'phone-number', id: 'call-me-anytime' }, - { type: 'phone-number', id: 'call-me-anytime' }, - ], + const person = store.push({ + data: { + id: '1', + type: 'phone-user', + attributes: {}, + relationships: { + phoneNumbers: { + data: [ + { type: 'phone-number', id: 'call-me-anytime' }, + { type: 'phone-number', id: 'call-me-anytime' }, + { type: 'phone-number', id: 'call-me-anytime' }, + ], + }, }, }, - }, - }); + }); - assert.strictEqual(person.phoneNumbers.length, 1); - }); + assert.strictEqual(person.phoneNumbers.length, 1); + } + ); deprecatedTest( 'a synchronous hasMany record array should only remove object(s) if found in collection', @@ -4503,7 +4381,7 @@ If using this relationship in a polymorphic manner is desired, the relationships ], }); - let recordArray = tag.people; + const recordArray = tag.people; recordArray.removeObject(scumbagNotInRecordArray); @@ -4520,7 +4398,7 @@ If using this relationship in a polymorphic manner is desired, the relationships recordArray.push(scumbagInRecordArray); - let scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; recordArray.removeObjects(scumbagsToRemove); didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); @@ -4591,7 +4469,7 @@ If using this relationship in a polymorphic manner is desired, the relationships ], }); - let recordArray = tag.people; + const recordArray = tag.people; recordArray.removeObject(scumbagNotInRecordArray); @@ -4608,7 +4486,7 @@ If using this relationship in a polymorphic manner is desired, the relationships recordArray.pushObject(scumbagInRecordArray); - let scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; recordArray.removeObjects(scumbagsToRemove); didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); 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 07acff34fec..3641620be67 100644 --- a/tests/main/tests/integration/relationships/inverse-relationship-load-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationship-load-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -13,7 +12,7 @@ module('inverse relationship load test', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; store = owner.lookup('service:store'); owner.register( 'serializer:application', @@ -29,14 +28,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -74,7 +73,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -91,18 +90,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.person; + 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' ); - let dogPerson2 = await dogs.at(1).person; + const dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -119,14 +118,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -164,7 +163,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -181,18 +180,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.person; + 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' ); - let dogPerson2 = await dogs.at(1).person; + const dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -206,14 +205,14 @@ module('inverse relationship load test', function (hooks) { ); 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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -253,7 +252,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -270,18 +269,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.pal; + 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' ); - let dogPerson2 = await dogs.at(1).pal; + const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -294,14 +293,14 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-many (left hand async, right hand sync) - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -341,7 +340,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -358,18 +357,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.pal; + 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' ); - let dogPerson2 = await dogs.at(1).pal; + const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -382,18 +381,18 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-many - findHasMany/null inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -416,7 +415,7 @@ module('inverse relationship load test', function (hooks) { ); class Person extends Model { - @hasMany('dogs', { + @hasMany('dog', { inverse: null, async: true, }) @@ -433,7 +432,7 @@ module('inverse relationship load test', function (hooks) { owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -450,7 +449,7 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.dogs; + const dogs = await person.dogs; assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); assert.strictEqual(dogs.length, 2); assert.deepEqual( @@ -458,7 +457,7 @@ module('inverse relationship load test', function (hooks) { ['1', '2'] ); - let dog1 = dogs.at(0); + const dog1 = dogs.at(0); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1); assert.strictEqual(dogs.at(0).id, '2'); @@ -468,18 +467,18 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findBelongsTo() { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -508,7 +507,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -528,7 +527,7 @@ module('inverse relationship load test', function (hooks) { let favoriteDog = await person.favoriteDog; assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); - let favoriteDogPerson = await favoriteDog.person; + const favoriteDogPerson = await favoriteDog.person; assert.strictEqual( favoriteDogPerson.id, '1', @@ -544,18 +543,18 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findBelongsTo() { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -584,7 +583,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -604,7 +603,7 @@ module('inverse relationship load test', function (hooks) { let favoriteDog = await person.favoriteDog; assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); - let favoriteDogPerson = await favoriteDog.person; + const favoriteDogPerson = await favoriteDog.person; assert.strictEqual( favoriteDogPerson.id, '1', @@ -617,18 +616,18 @@ module('inverse relationship load test', function (hooks) { ); test('one-to-one - findBelongsTo/explicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findBelongsTo() { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -657,7 +656,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -677,7 +676,7 @@ module('inverse relationship load test', function (hooks) { let favoriteDog = await person.favoriteDog; assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); - let favoriteDogPerson = await favoriteDog.pal; + const favoriteDogPerson = await favoriteDog.pal; assert.strictEqual( favoriteDogPerson.id, '1', @@ -689,18 +688,18 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-one (left hand async, right hand sync) - findBelongsTo/explicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findBelongsTo() { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -729,7 +728,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -749,7 +748,7 @@ module('inverse relationship load test', function (hooks) { let favoriteDog = await person.favoriteDog; assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); - let favoriteDogPerson = await favoriteDog.pal; + const favoriteDogPerson = await favoriteDog.pal; assert.strictEqual( favoriteDogPerson.id, '1', @@ -761,18 +760,18 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-one - findBelongsTo/null inverse - ensures inverse relationship is set up when payload does not return parent relationship info', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ deleteRecord() { - return resolve({ + return Promise.resolve({ data: null, }); }, findBelongsTo() { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -799,7 +798,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -828,14 +827,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -873,7 +872,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -890,16 +889,16 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let [dog1, dog2] = dogs.slice(); - let dog1Walkers = await dog1.walkers; + 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'); - let dog2Walkers = await dog2.walkers; + 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'); @@ -913,14 +912,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -958,7 +957,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -975,16 +974,16 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let [dog1, dog2] = dogs.slice(); - let dog1Walkers = await dog1.walkers; + 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'); - let dog2Walkers = await dog2.walkers; + 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'); @@ -995,14 +994,14 @@ module('inverse relationship load test', function (hooks) { ); 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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1042,7 +1041,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1059,16 +1058,16 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let [dog1, dog2] = dogs.slice(); - let dog1Pals = await dog1.pals; + const [dog1, dog2] = dogs.slice(); + const dog1Pals = await dog1.pals; assert.strictEqual(dog1Pals.length, 1, 'dog1.pals inverse relationship includes correct number of records'); assert.strictEqual(dog1Pals.at(0).id, '1', 'dog1.pals inverse relationship is set up correctly'); - let dog2Pals = await dog2.pals; + const dog2Pals = await dog2.pals; assert.strictEqual(dog2Pals.length, 1, 'dog2.pals inverse relationship includes correct number of records'); assert.strictEqual(dog2Pals.at(0).id, '1', 'dog2.pals inverse relationship is set up correctly'); @@ -1078,14 +1077,14 @@ module('inverse relationship load test', function (hooks) { }); test('many-to-many (left hand async, right hand sync) - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1125,7 +1124,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1142,16 +1141,16 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let [dog1, dog2] = dogs.slice(); - let dog1Pals = await dog1.pals; + const [dog1, dog2] = dogs.slice(); + const dog1Pals = await dog1.pals; assert.strictEqual(dog1Pals.length, 1, 'dog1.pals inverse relationship includes correct number of records'); assert.strictEqual(dog1Pals.at(0).id, '1', 'dog1.pals inverse relationship is set up correctly'); - let dog2Pals = await dog2.pals; + const dog2Pals = await dog2.pals; assert.strictEqual(dog2Pals.length, 1, 'dog2.pals inverse relationship includes correct number of records'); assert.strictEqual(dog2Pals.at(0).id, '1', 'dog2.pals inverse relationship is set up correctly'); @@ -1164,14 +1163,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -1217,17 +1216,17 @@ module('inverse relationship load test', function (hooks) { }, }); - let person = await dog.person; + 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'); - let dogs = await person.dogs; + const dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.slice(); + const [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1240,14 +1239,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -1293,17 +1292,17 @@ module('inverse relationship load test', function (hooks) { }, }); - let person = await dog.person; + 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'); - let dogs = await person.dogs; + const dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.slice(); + const [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1313,14 +1312,14 @@ module('inverse relationship load test', function (hooks) { ); 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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -1368,17 +1367,17 @@ module('inverse relationship load test', function (hooks) { }, }); - let person = await dog.pal; + const person = await dog.pal; assert.false( dog.belongsTo('pal').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); - let dogs = await person.dogs; + const dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.slice(); + const [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1387,14 +1386,14 @@ module('inverse relationship load test', function (hooks) { }); test('many-to-one (left hand async, right hand sync) - findBelongsTo/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -1442,17 +1441,17 @@ module('inverse relationship load test', function (hooks) { }, }); - let person = await dog.pal; + const person = await dog.pal; assert.false( dog.belongsTo('pal').belongsToRelationship.state.isEmpty, 'belongsTo relationship state was populated' ); assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); - let dogs = await person.dogs; + const dogs = await person.dogs; assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); - let [dog1] = dogs.slice(); + const [dog1] = dogs.slice(); assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); await person.destroyRecord(); @@ -1464,14 +1463,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1525,7 +1524,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1552,14 +1551,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1613,7 +1612,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1640,14 +1639,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1695,7 +1694,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1722,14 +1721,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -1777,7 +1776,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1804,14 +1803,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -1848,7 +1847,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1875,14 +1874,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -1919,7 +1918,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -1946,14 +1945,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -1987,7 +1986,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2014,14 +2013,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'dog', @@ -2055,7 +2054,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2082,14 +2081,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -2128,7 +2127,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let dog = store.push({ + const dog = store.push({ data: { type: 'dog', id: '1', @@ -2155,14 +2154,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -2201,7 +2200,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let dog = store.push({ + const dog = store.push({ data: { type: 'dog', id: '1', @@ -2228,14 +2227,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -2269,7 +2268,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let dog = store.push({ + const dog = store.push({ data: { type: 'dog', id: '1', @@ -2296,14 +2295,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -2337,7 +2336,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let dog = store.push({ + const dog = store.push({ data: { type: 'dog', id: '1', @@ -2364,14 +2363,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2429,7 +2428,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person1 = store.push({ + const person1 = store.push({ data: { type: 'person', id: '1', @@ -2456,14 +2455,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2521,7 +2520,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person1 = store.push({ + const person1 = store.push({ data: { type: 'person', id: '1', @@ -2548,14 +2547,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2603,7 +2602,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2630,14 +2629,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2685,7 +2684,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2712,14 +2711,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2767,7 +2766,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2794,14 +2793,14 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findHasMany: () => { - return resolve({ + return Promise.resolve({ data: [ { id: '1', @@ -2849,7 +2848,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2876,7 +2875,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -2897,10 +2896,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -2923,7 +2922,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -2947,18 +2946,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.person; + 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' ); - let dogPerson2 = await dogs.at(1).person; + const dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -2975,7 +2974,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -2996,10 +2995,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3022,7 +3021,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3046,18 +3045,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.person; + 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' ); - let dogPerson2 = await dogs.at(1).person; + const dogPerson2 = await dogs.at(1).person; assert.strictEqual( dogPerson2.id, '1', @@ -3071,7 +3070,7 @@ module('inverse relationship load test', function (hooks) { ); 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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3092,10 +3091,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3120,7 +3119,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3144,18 +3143,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.pal; + 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' ); - let dogPerson2 = await dogs.at(1).pal; + const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -3168,7 +3167,7 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-many (left hand async, right hand sync) - 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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3189,10 +3188,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3217,7 +3216,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3241,18 +3240,18 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); - let dogPerson1 = await dog1.pal; + 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' ); - let dogPerson2 = await dogs.at(1).pal; + const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, '1', @@ -3265,7 +3264,7 @@ module('inverse relationship load test', function (hooks) { }); test('one-to-many - ids/non-link/null 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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3286,10 +3285,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3308,7 +3307,7 @@ module('inverse relationship load test', function (hooks) { class Dog extends Model {} owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3332,11 +3331,11 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.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'); - let dog1 = dogs.at(0); + const dog1 = dogs.at(0); await dog1.destroyRecord(); assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); @@ -3347,7 +3346,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3384,10 +3383,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3410,7 +3409,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3434,7 +3433,7 @@ module('inverse relationship load test', function (hooks) { }, }); - let person2 = store.push({ + const person2 = store.push({ data: { type: 'person', id: '2', @@ -3444,26 +3443,26 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.dogs; + 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'); - let person2Dogs = await person2.dogs; + const person2Dogs = await person2.dogs; assert.strictEqual( person2Dogs.length, 2, 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.person; + const dog = allDogs[i]; + const dogPerson = await dog.person; assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + 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'); @@ -3474,7 +3473,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3511,10 +3510,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3537,7 +3536,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3561,7 +3560,7 @@ module('inverse relationship load test', function (hooks) { }, }); - let person2 = store.push({ + const person2 = store.push({ data: { type: 'person', id: '2', @@ -3571,26 +3570,26 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await person.dogs; + 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'); - let person2Dogs = await person2.dogs; + const person2Dogs = await person2.dogs; assert.strictEqual( person2Dogs.length, 2, 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.person; + const dog = allDogs[i]; + const dogPerson = await dog.person; assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + 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'); @@ -3601,7 +3600,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3632,10 +3631,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3658,7 +3657,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3682,19 +3681,19 @@ module('inverse relationship load test', function (hooks) { }, }); - let personDogs = await person.dogs; + 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'); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.person; + const dog = allDogs[i]; + const dogPerson = await dog.person; assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + const dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(personDogs.length, 0); @@ -3705,7 +3704,7 @@ module('inverse relationship load test', function (hooks) { '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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3736,10 +3735,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3762,7 +3761,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -3786,19 +3785,19 @@ module('inverse relationship load test', function (hooks) { }, }); - let personDogs = await person.dogs; + 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'); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.person; + const dog = allDogs[i]; + const dogPerson = await dog.person; assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + const dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(personDogs.length, 0); @@ -3806,7 +3805,7 @@ module('inverse relationship load test', function (hooks) { ); 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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3843,10 +3842,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3871,7 +3870,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let pal = store.push({ + const pal = store.push({ data: { type: 'pal', id: '1', @@ -3895,7 +3894,7 @@ module('inverse relationship load test', function (hooks) { }, }); - let pal2 = store.push({ + const pal2 = store.push({ data: { type: 'pal', id: '2', @@ -3905,33 +3904,33 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await pal.dogs; + const dogs = await pal.dogs; assert.false(pal.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); - let pal2Dogs = await pal2.dogs; + const pal2Dogs = await pal2.dogs; assert.strictEqual( pal2Dogs.length, 2, 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.pal; + const dog = allDogs[i]; + const dogPerson = await dog.pal; assert.strictEqual(dogPerson.id, pal2.id, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + const dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(pal2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); assert.strictEqual(pal2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test('one-to-many (left hand async, right hand sync) - 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) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -3968,10 +3967,10 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -3996,7 +3995,7 @@ module('inverse relationship load test', function (hooks) { } owner.register('model:dog', Dog); - let pal = store.push({ + const pal = store.push({ data: { type: 'pal', id: '1', @@ -4020,7 +4019,7 @@ module('inverse relationship load test', function (hooks) { }, }); - let pal2 = store.push({ + const pal2 = store.push({ data: { type: 'pal', id: '2', @@ -4030,33 +4029,33 @@ module('inverse relationship load test', function (hooks) { }, }); - let dogs = await pal.dogs; + const dogs = await pal.dogs; assert.false(pal.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); - let pal2Dogs = await pal2.dogs; + const pal2Dogs = await pal2.dogs; assert.strictEqual( pal2Dogs.length, 2, 'hasMany relationship on specified record has correct number of associated records' ); - let allDogs = store.peekAll('dogs').slice(); + const allDogs = store.peekAll('dogs').slice(); for (let i = 0; i < allDogs.length; i++) { - let dog = allDogs[i]; - let dogPerson = await dog.pal; + const dog = allDogs[i]; + const dogPerson = await dog.pal; assert.strictEqual(dogPerson.id, pal2.id, 'right hand side has correct belongsTo value'); } - let dog1 = store.peekRecord('dog', '1'); + const dog1 = store.peekRecord('dog', '1'); await dog1.destroyRecord(); assert.strictEqual(pal2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); assert.strictEqual(pal2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); test("loading belongsTo doesn't remove inverse relationship for other instances", async function (assert) { - let { owner } = this; + const { owner } = this; const scooby = { id: '1', @@ -4087,9 +4086,9 @@ module('inverse relationship load test', function (hooks) { owner.register( 'adapter:application', JSONAPIAdapter.extend({ - deleteRecord: () => resolve({ data: null }), + deleteRecord: () => Promise.resolve({ data: null }), findBelongsTo: () => { - return resolve({ + return Promise.resolve({ data: { type: 'person', id: '1', @@ -4108,7 +4107,7 @@ module('inverse relationship load test', function (hooks) { }, findRecord: (_store, _type, id) => { const dog = id === '1' ? scooby : scrappy; - return resolve({ + return Promise.resolve({ data: dog, }); }, @@ -4139,8 +4138,8 @@ module('inverse relationship load test', function (hooks) { owner.register('model:dog', Dog); // load em into store - let dog1 = await owner.lookup('service:store').findRecord('dog', '1'); - let dog2 = await owner.lookup('service:store').findRecord('dog', '2'); + const dog1 = await owner.lookup('service:store').findRecord('dog', '1'); + const dog2 = await owner.lookup('service:store').findRecord('dog', '2'); assert.strictEqual(dog1.belongsTo('person').id(), '1'); assert.strictEqual(dog2.belongsTo('person').id(), '1'); diff --git a/tests/main/tests/integration/relationships/inverse-relationships-test.js b/tests/main/tests/integration/relationships/inverse-relationships-test.js index 503d48984a2..b2e143bc78d 100644 --- a/tests/main/tests/integration/relationships/inverse-relationships-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationships-test.js @@ -7,6 +7,7 @@ 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); @@ -157,10 +158,8 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' register('model:Post', Post); register('model:Comment', Comment); - let comment, post; - - comment = store.createRecord('comment'); - post = store.createRecord('post'); + 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'); @@ -454,10 +453,15 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' store.createRecord('comment'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.comments; - }, /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./); + 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) { @@ -477,10 +481,15 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' let post; store.createRecord('user'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.user; - }, /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./); + 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) { @@ -634,9 +643,14 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' } register('model:user', User); - assert.expectAssertion(() => { - store.createRecord('user', { post: null }); - }, /No model was found for 'post' and no schema handles the 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', {}); diff --git a/tests/main/tests/integration/relationships/json-api-links-test.js b/tests/main/tests/integration/relationships/json-api-links-test.js index cd44c912959..b5693aee6f1 100644 --- a/tests/main/tests/integration/relationships/json-api-links-test.js +++ b/tests/main/tests/integration/relationships/json-api-links-test.js @@ -1,7 +1,6 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -9,7 +8,6 @@ import Adapter from '@ember-data/adapter'; 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 deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; module('integration/relationship/json-api-links | Relationship state updates', function (hooks) { setupTest(hooks); @@ -28,13 +26,13 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); this.owner.register( 'adapter:user', class extends JSONAPISerializer { findRecord(store, type, id) { - return resolve({ + return Promise.resolve({ data: { id, type: 'user', @@ -53,7 +51,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f 'adapter:organisation', class extends JSONAPISerializer { findRecord(store, type, id) { - return resolve({ + return Promise.resolve({ data: { type: 'organisation', id, @@ -101,7 +99,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const parent = store.push({ data: { @@ -155,7 +153,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f }, findRecord(_, __, id) { assert.notStrictEqual(id, '1', `adapter findRecord called for all IDs except "1", called for "${id}"`); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id, @@ -175,7 +173,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push data, no links store.push({ @@ -210,7 +208,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f await settled(); - let chris = store.peekRecord('user', '1'); + const chris = store.peekRecord('user', '1'); await chris.pets; }); @@ -225,7 +223,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f const Adapter = JSONAPIAdapter.extend({ findHasMany(_, __, link) { assert.strictEqual(link, './user/1/pets', 'We fetched via the correct link'); - return resolve({ + return Promise.resolve({ data: [ { type: 'pet', @@ -262,7 +260,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push data, no links store.push({ @@ -292,7 +290,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f }, }); - let chris = store.peekRecord('user', '1'); + const chris = store.peekRecord('user', '1'); await chris.pets; }); @@ -307,7 +305,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f const Adapter = JSONAPIAdapter.extend({ findHasMany(_, __, link) { assert.strictEqual(link, './user/1/pets', 'We fetched via the correct link'); - return resolve({ + return Promise.resolve({ data: [ { type: 'pet', @@ -344,7 +342,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push links, no data store.push({ @@ -391,7 +389,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f const Adapter = JSONAPIAdapter.extend({ findHasMany(_, __, link) { assert.strictEqual(link, './user/1/pets', 'We fetched via the correct link'); - return resolve({ + return Promise.resolve({ data: [ { type: 'pet', @@ -428,7 +426,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push links, no data @@ -478,7 +476,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f const Adapter = JSONAPIAdapter.extend({ findHasMany(_, __, link) { assert.strictEqual(link, './user/1/pets', 'We fetched via the correct link'); - return resolve({ + return Promise.resolve({ data: [ { type: 'pet', @@ -515,7 +513,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push data and links store.push({ @@ -548,7 +546,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f const Adapter = JSONAPIAdapter.extend({ findHasMany(_, __, link) { assert.strictEqual(link, './user/1/pets', 'We fetched via the correct link'); - return resolve({ + return Promise.resolve({ data: [ { type: 'pet', @@ -585,7 +583,7 @@ module('integration/relationship/json-api-links | Relationship state updates', f this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // push data, no links store.push({ @@ -675,8 +673,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload hasMany with ${description}`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -691,12 +689,12 @@ module('integration/relationship/json-api-links | Relationship fetching', functi payloads.user.data.relationships.pets.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.pets)); + return Promise.resolve(structuredClone(payloads.pets)); }; // setup user - let user = store.push(deepCopy(payloads.user)); - let pets = await user.pets; + const user = store.push(structuredClone(payloads.user)); + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); @@ -706,11 +704,11 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get hasMany with ${description}`, async function (assert) { assert.expect(5); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let petRelationshipData = payloads.user.data.relationships.pets.data; - let petRelDataWasEmpty = petRelationshipData && petRelationshipData.length === 0; + const petRelationshipData = payloads.user.data.relationships.pets.data; + const petRelDataWasEmpty = petRelationshipData && petRelationshipData.length === 0; adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -733,12 +731,12 @@ module('integration/relationship/json-api-links | Relationship fetching', functi 'We fetched the appropriate link' ); } - return resolve(deepCopy(payloads.pets)); + return Promise.resolve(structuredClone(payloads.pets)); }; // setup user - let user = store.push(deepCopy(payloads.user)); - let pets = await user.pets; + const user = store.push(structuredClone(payloads.user)); + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); @@ -755,11 +753,11 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload belongsTo with ${description}`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let homeRelationshipData = payloads.user.data.relationships.home.data; - let homeRelWasEmpty = homeRelationshipData === null; + const homeRelationshipData = payloads.user.data.relationships.home.data; + const homeRelWasEmpty = homeRelationshipData === null; let isInitialFetch = true; let didFetchInitially = false; @@ -781,12 +779,12 @@ module('integration/relationship/json-api-links | Relationship fetching', functi 'We fetched the appropriate link' ); } - return resolve(deepCopy(payloads.home)); + return Promise.resolve(structuredClone(payloads.home)); }; // setup user - let user = store.push(deepCopy(payloads.user)); - let home = user.home; + const user = store.push(structuredClone(payloads.user)); + const home = user.home; await home; if (homeRelWasEmpty) { @@ -802,11 +800,11 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get belongsTo with ${description}`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let homeRelationshipData = payloads.user.data.relationships.home.data; - let homeRelWasEmpty = homeRelationshipData === null; + const homeRelationshipData = payloads.user.data.relationships.home.data; + const homeRelWasEmpty = homeRelationshipData === null; adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -820,12 +818,12 @@ module('integration/relationship/json-api-links | Relationship fetching', functi !homeRelWasEmpty && link === payloads.user.data.relationships.home.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.home)); + return Promise.resolve(structuredClone(payloads.home)); }; // setup user - let user = store.push(deepCopy(payloads.user)); - let home = await user.home; + const user = store.push(structuredClone(payloads.user)); + const home = await user.home; assert.ok(!!home, 'We found our home'); @@ -976,8 +974,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload hasMany with ${description}`, async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -992,13 +990,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi payloads.user.data.relationships.pets.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.pets)); + return Promise.resolve(structuredClone(payloads.pets)); }; // setup user and pets - let user = store.push(deepCopy(payloads.user)); - store.push(deepCopy(payloads.pets)); - let pets = await user.pets; + const user = store.push(structuredClone(payloads.user)); + store.push(structuredClone(payloads.pets)); + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); @@ -1008,8 +1006,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get hasMany with ${description}`, async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -1024,13 +1022,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi payloads.user.data.relationships.pets.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.pets)); + return Promise.resolve(structuredClone(payloads.pets)); }; // setup user and pets - let user = store.push(deepCopy(payloads.user)); - store.push(deepCopy(payloads.pets)); - let pets = await user.pets; + const user = store.push(structuredClone(payloads.user)); + store.push(structuredClone(payloads.pets)); + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); @@ -1043,8 +1041,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload belongsTo with ${description}`, async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -1059,14 +1057,14 @@ module('integration/relationship/json-api-links | Relationship fetching', functi payloads.user.data.relationships.home.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.home)); + return Promise.resolve(structuredClone(payloads.home)); }; // setup user and home - let user = store.push(deepCopy(payloads.user)); - store.push(deepCopy(payloads.home)); + const user = store.push(structuredClone(payloads.user)); + store.push(structuredClone(payloads.home)); await settled(); - let home = user.home; + const home = user.home; await home; @@ -1078,8 +1076,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get belongsTo with ${description}`, async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -1094,14 +1092,14 @@ module('integration/relationship/json-api-links | Relationship fetching', functi payloads.user.data.relationships.home.links.related, 'We fetched the appropriate link' ); - return resolve(deepCopy(payloads.home)); + return Promise.resolve(structuredClone(payloads.home)); }; // setup user - let user = store.push(deepCopy(payloads.user)); - store.push(deepCopy(payloads.home)); + const user = store.push(structuredClone(payloads.user)); + store.push(structuredClone(payloads.home)); await settled(); - let home = await user.home; + const home = await user.home; assert.ok(!!home, 'We found our home'); @@ -1311,13 +1309,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload hasMany with data, no links`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '1', @@ -1343,7 +1341,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1360,7 +1358,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }); - let pets = await user.pets; + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); @@ -1370,13 +1368,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get hasMany with data, no links`, async function (assert) { assert.expect(5); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '1', @@ -1401,7 +1399,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi assert.ok(false, 'We should not call findHasMany'); }; - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1418,7 +1416,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }); - let pets = await user.pets; + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); pets.at(0).unloadRecord(); @@ -1430,13 +1428,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload belongsTo with data, no links`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'home', id: '1', @@ -1462,7 +1460,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1479,7 +1477,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }); - let home = user.home; + const home = user.home; await home; assert.ok(!!home, 'We found our home'); @@ -1489,13 +1487,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get belongsTo with data, no links`, async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'home', id: '1', @@ -1521,7 +1519,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1538,7 +1536,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }); - let home = await user.home; + const home = await user.home; assert.ok(!!home, 'We found our home'); @@ -1551,13 +1549,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload hasMany with missing data setup from the other side, no links`, async function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '1', @@ -1583,7 +1581,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user and pet - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1610,7 +1608,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, ], }); - let pets = await user.pets; + const pets = await user.pets; assert.strictEqual(pets.length, 1, 'we setup the pets'); assert.ok(!!pets, 'We found our pets'); @@ -1621,13 +1619,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get hasMany with missing data setup from the other side, no links`, async function (assert) { assert.expect(5); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'pet', id: '1', @@ -1653,7 +1651,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user and pet - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1683,7 +1681,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi // should not trigger a fetch bc even though we don't consider `pets` to have complete knowledge // we have no knowledge with which to initate a request. - let pets = await user.pets; + const pets = await user.pets; assert.ok(!!pets, 'We found our pets'); assert.strictEqual(pets.length, 1, 'we loaded our pets'); @@ -1698,13 +1696,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload belongsTo with missing data setup from the other side, no links`, async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'home', id: '1', @@ -1757,7 +1755,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, ], }); - let home = user.home; + const home = user.home; await home; @@ -1768,13 +1766,13 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+unload+get belongsTo with missing data setup from the other side, no links`, async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { assert.ok(true, 'We should call findRecord'); - return resolve({ + return Promise.resolve({ data: { type: 'home', id: '1', @@ -1827,8 +1825,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, ], }); - let home = user.home; - let h = await home; + const home = user.home; + const h = await home; assert.ok(!!home, 'We found our home'); @@ -1841,8 +1839,8 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test(`get+reload hasMany with empty data, no links`, async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.findRecord = () => { @@ -1856,7 +1854,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup user - let user = store.push({ + const user = store.push({ data: { type: 'user', id: '1', @@ -1873,7 +1871,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }); - let pets = user.pets; + const pets = user.pets; await pets; assert.ok(!!pets, 'We found our pets'); @@ -1886,10 +1884,10 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test('We should not fetch a hasMany relationship with links that we know is empty', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let user1Payload = { + const user1Payload = { data: { type: 'user', id: '1', @@ -1906,7 +1904,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }, }, }; - let user2Payload = { + const user2Payload = { data: { type: 'user', id: '2', @@ -1944,14 +1942,14 @@ module('integration/relationship/json-api-links | Relationship fetching', functi ); } - return resolve({ + return Promise.resolve({ data: [], }); }; // setup users - let user1 = store.push(deepCopy(user1Payload)); - let user2 = store.push(deepCopy(user2Payload)); + const user1 = store.push(structuredClone(user1Payload)); + const user2 = store.push(structuredClone(user2Payload)); // should not fire a request requestedUser = null; @@ -1976,10 +1974,10 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test('We should not fetch a sync hasMany relationship with a link that is missing the data member', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let petPayload = { + const petPayload = { data: { type: 'pet', id: '1', @@ -2011,7 +2009,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup users - let shen = store.push(petPayload); + const shen = store.push(petPayload); // should not fire a request await shen.pets; @@ -2022,10 +2020,10 @@ module('integration/relationship/json-api-links | Relationship fetching', functi test('We should not fetch a sync belongsTo relationship with a link that is missing the data member', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let petPayload = { + const petPayload = { data: { type: 'pet', id: '1', @@ -2058,7 +2056,7 @@ module('integration/relationship/json-api-links | Relationship fetching', functi }; // setup users - let shen = store.push(petPayload); + const shen = store.push(petPayload); // should not fire a request await shen.owner; diff --git a/tests/main/tests/integration/relationships/many-to-many-test.js b/tests/main/tests/integration/relationships/many-to-many-test.js index 9fd23c24d16..0d5f71a867a 100644 --- a/tests/main/tests/integration/relationships/many-to-many-test.js +++ b/tests/main/tests/integration/relationships/many-to-many-test.js @@ -45,7 +45,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', */ test('Loading from one hasMany side reflects on the other hasMany side - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -71,7 +71,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }); - let topic = store.push({ + const topic = store.push({ data: { id: '2', type: 'topic', @@ -87,9 +87,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Relationship is available from one hasMany side even if only loaded from the other hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let account = store.push({ + const account = store.push({ data: { id: '2', type: 'account', @@ -121,9 +121,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Fetching a hasMany where a record was removed reflects on the other hasMany side - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -137,7 +137,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let topic = store.push({ + const topic = store.push({ data: { id: '2', type: 'topic', @@ -161,7 +161,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Fetching a hasMany where a record was removed reflects on the other hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let account = store.push({ data: { @@ -172,7 +172,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -217,9 +217,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', test('Pushing to a hasMany reflects on the other hasMany side - async', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -233,7 +233,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let topic = store.push({ + const topic = store.push({ data: { id: '2', type: 'topic', @@ -250,9 +250,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Pushing to a hasMany reflects on the other hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let account = store.push({ + const account = store.push({ data: { id: '2', type: 'account', @@ -261,7 +261,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let stanley = store.push({ + const stanley = store.push({ data: { id: '1', type: 'user', @@ -276,7 +276,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Removing a record from a hasMany reflects on the other hasMany side - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const user = store.push({ data: { @@ -316,7 +316,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Removing a record from a hasMany reflects on the other hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const account = store.push({ data: { @@ -358,9 +358,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', */ test('Rollbacking attributes for a deleted record that has a ManyToMany relationship works correctly - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -379,7 +379,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let topic = store.push({ + const topic = store.push({ data: { id: '2', type: 'topic', @@ -393,17 +393,17 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', topic.rollbackAttributes(); await settled(); - let users = await topic.users; + const users = await topic.users; assert.strictEqual(users.length, 1, 'Users are still there'); - let topics = await user.topics; + const topics = await user.topics; assert.strictEqual(topics.length, 1, 'Topic got rollbacked into the user'); }); test('Deleting a record that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let account = store.push({ + const account = store.push({ data: { id: '2', type: 'account', @@ -412,7 +412,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -438,9 +438,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Rollbacking attributes for a created record that has a ManyToMany relationship works correctly - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -449,13 +449,13 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let topic = store.createRecord('topic'); + const topic = store.createRecord('topic'); let fetchedTopics = await user.topics; fetchedTopics.push(topic); topic.rollbackAttributes(); - let fetchedUsers = await topic.users; + const fetchedUsers = await topic.users; assert.strictEqual(fetchedUsers.length, 0, 'Users got removed'); assert.strictEqual(fetchedUsers.at(0), undefined, "User can't be fetched"); @@ -465,9 +465,9 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }); test('Deleting an unpersisted record via rollbackAttributes that has a hasMany relationship removes it from the otherMany array but does not remove the other record from itself - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let account = store.push({ + const account = store.push({ data: { id: '2', type: 'account', @@ -477,7 +477,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }); - let user = store.createRecord('user'); + const user = store.createRecord('user'); account.users.push(user); user.rollbackAttributes(); @@ -491,7 +491,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', function (assert) { assert.expect(4); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let account = store.push({ data: { @@ -502,7 +502,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let ada = store.push({ + const ada = store.push({ data: { id: '1', type: 'user', @@ -521,7 +521,7 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }, }); - let byron = store.push({ + const byron = store.push({ data: { id: '2', type: 'user', @@ -565,10 +565,10 @@ module('integration/relationships/many_to_many_test - ManyToMany relationships', }, }); - let state = account.hasMany('users').hasManyRelationship.remoteState; - let users = account.users; + const state = account.hasMany('users').hasManyRelationship.remoteState; + const users = account.users; - assert.todo.equal(users.length, 1, 'Accounts were updated correctly (ui state)'); + assert.todo.strictEqual(users.length, 1, 'Accounts were updated correctly (ui state)'); assert.todo.deepEqual( users.map((r) => get(r, 'id')), ['1'], diff --git a/tests/main/tests/integration/relationships/nested-relationship-test.js b/tests/main/tests/integration/relationships/nested-relationship-test.js index c9332d32e17..6cd3c41478a 100644 --- a/tests/main/tests/integration/relationships/nested-relationship-test.js +++ b/tests/main/tests/integration/relationships/nested-relationship-test.js @@ -36,8 +36,8 @@ module('integration/relationships/nested_relationships_test - Nested relationshi */ test('Sideloaded nested relationships load correctly', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => { return false; @@ -123,18 +123,18 @@ module('integration/relationships/nested_relationships_test - Nested relationshi ], }); - let kid = store.peekRecord('kid', '1'); + const kid = store.peekRecord('kid', '1'); const middleAger = await kid.middleAger; assert.ok(middleAger, 'MiddleAger relationship was set up correctly'); - let middleAgerName = middleAger.name; - let kids = await middleAger.kids; + const middleAgerName = middleAger.name; + const kids = await middleAger.kids; assert.strictEqual(middleAgerName, 'Middle Ager 1', 'MiddleAger name is there'); assert.ok(kids.includes(kid)); const elder = await middleAger.elder; assert.notEqual(elder, null, 'Elder relationship was set up correctly'); - let elderName = elder.name; + const elderName = elder.name; assert.strictEqual(elderName, 'Elder 1', 'Elder name is there'); }); }); 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 7fd00fb602f..ce59fd8fa13 100644 --- a/tests/main/tests/integration/relationships/one-to-many-test.js +++ b/tests/main/tests/integration/relationships/one-to-many-test.js @@ -1,8 +1,4 @@ -import { get } from '@ember/object'; -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -32,7 +28,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); const ApplicationAdapter = Adapter.extend({ - deleteRecord: () => resolve(), + deleteRecord: () => Promise.resolve(), }); this.owner.register('model:user', User); @@ -47,49 +43,45 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f Server loading tests */ - test('Relationship is available from the belongsTo side even if only loaded from the hasMany side - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Relationship is available from the belongsTo side even if only loaded from the hasMany side - async', async function (assert) { + const store = this.owner.lookup('service:store'); var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '2', - type: 'message', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '2', - type: 'message', - attributes: { - title: 'EmberFest was great', + relationships: { + messages: { + data: [ + { + id: '2', + type: 'message', + }, + ], }, }, - }); + }, }); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, user, 'User relationship was set up correctly'); - }); + message = store.push({ + data: { + id: '2', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + }, + }); + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, user, 'User relationship was set up correctly'); }); }); test("Adapter's findBelongsTo must not be hit when the record is included with its owner", async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expect(1); this.owner.register( @@ -147,228 +139,156 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); test('Relationship is available from the belongsTo side even if only loaded from the hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var account, user; - run(function () { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); + }, }); assert.strictEqual(account.user, user, 'User relationship was set up correctly'); }); - test('Relationship is available from the hasMany side even if only loaded from the belongsTo side - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Relationship is available from the hasMany side even if only loaded from the belongsTo side - async', async function (assert) { + const store = this.owner.lookup('service:store'); var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '2', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { - id: '1', - type: 'user', - }, + }, + }); + message = store.push({ + data: { + id: '2', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); + }, }); - run(function () { - user.messages.then(function (fetchedMessages) { - assert.strictEqual(fetchedMessages.at(0), message, 'Messages relationship was set up correctly'); - }); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.at(0), message, 'Messages relationship was set up correctly'); }); }); test('Relationship is available from the hasMany side even if only loaded from the belongsTo side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, - relationships: { - user: { - data: { - id: '1', - type: 'user', - }, + }, + }); + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); - }); - run(function () { - assert.strictEqual(user.accounts.at(0), account, 'Accounts relationship was set up correctly'); + }, }); + assert.strictEqual(user.accounts.at(0), account, 'Accounts relationship was set up correctly'); }); - test('Fetching a belongsTo that is set to null removes the record from a relationship - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching a belongsTo that is set to null removes the record from a relationship - async', async function (assert) { + const store = this.owner.lookup('service:store'); - var user; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - { - id: '2', - type: 'message', - }, - ], - }, - }, + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - }); - run(function () { - store.push({ - data: [ - { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { - id: '1', - type: 'user', - }, + relationships: { + messages: { + data: [ + { + id: '1', + type: 'message', }, - }, - }, - { - id: '2', - type: 'message', - attributes: { - title: 'EmberConf will be better', - }, - relationships: { - user: { - data: null, + { + id: '2', + type: 'message', }, - }, - }, - ], - }); - }); - run(function () { - user.messages.then(function (fetchedMessages) { - assert.strictEqual(get(fetchedMessages, 'length'), 1, 'Messages relationship was set up correctly'); - }); - }); - }); - - test('Fetching a belongsTo that is set to null removes the record from a relationship - sync', function (assert) { - let store = this.owner.lookup('service:store'); - - var user; - run(function () { - store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', + ], }, }, - }); - - user = store.push({ - data: { + }, + }); + store.push({ + data: [ + { id: '1', - type: 'user', + type: 'message', attributes: { - name: 'Stanley', + title: 'EmberFest was great', }, relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], + user: { + data: { + id: '1', + type: 'user', + }, }, }, }, - }); - - store.push({ - data: { + { id: '2', - type: 'account', + type: 'message', attributes: { - state: 'lonely', + title: 'EmberConf will be better', }, relationships: { user: { @@ -376,407 +296,424 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }, - }); + ], }); - - run(function () { - assert.strictEqual(user.accounts.at(0), undefined, 'Account was sucesfully removed'); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 1, 'Messages relationship was set up correctly'); }); }); - test('Fetching a belongsTo that is not defined does not remove the record from a relationship - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching a belongsTo that is set to null removes the record from a relationship - sync', function (assert) { + const store = this.owner.lookup('service:store'); var user; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - { - id: '2', - type: 'message', - }, - ], - }, - }, + store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); + }, }); - run(function () { - store.push({ - data: [ - { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { - id: '1', - type: 'user', - }, + + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', }, - }, - }, - { - id: '2', - type: 'message', - attributes: { - title: 'EmberConf will be better', - }, + ], }, - ], - }); + }, + }, }); - run(function () { - user.messages.then(function (fetchedMessages) { - assert.strictEqual(get(fetchedMessages, 'length'), 2, 'Messages relationship was set up correctly'); - }); + + store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', + }, + relationships: { + user: { + data: null, + }, + }, + }, }); + + assert.strictEqual(user.accounts.at(0), undefined, 'Account was successfully removed'); }); - test('Fetching a belongsTo that is not defined does not remove the record from a relationship - sync', function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching a belongsTo that is not defined does not remove the record from a relationship - async', async function (assert) { + const store = this.owner.lookup('service:store'); - var account, user; - run(function () { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', + var user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + messages: { + data: [ + { + id: '1', + type: 'message', + }, + { + id: '2', + type: 'message', + }, + ], }, }, - }); - user = store.push({ - data: { + }, + }); + store.push({ + data: [ + { id: '1', - type: 'user', + type: 'message', attributes: { - name: 'Stanley', + title: 'EmberFest was great', }, relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], + user: { + data: { + id: '1', + type: 'user', + }, }, }, }, - }); - account = store.push({ - data: { + { id: '2', - type: 'account', + type: 'message', attributes: { - state: 'lonely', + title: 'EmberConf will be better', }, }, - }); + ], }); - - run(function () { - assert.strictEqual(user.accounts.at(0), account, 'Account was sucesfully removed'); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 2, 'Messages relationship was set up correctly'); }); }); - test("Fetching the hasMany that doesn't contain the belongsTo, sets the belongsTo to null - async", function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching a belongsTo that is not defined does not remove the record from a relationship - sync', function (assert) { + const store = this.owner.lookup('service:store'); - let user, message, message2; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - ], - }, - }, + var account, user; + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - message = store.push({ - data: { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, - relationships: { - user: { - data: { + }, + }, + }); + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', + }, + }, + }); + + assert.strictEqual(user.accounts.at(0), account, 'Account was successfully removed'); + }); + + test("Fetching the hasMany that doesn't contain the belongsTo, sets the belongsTo to null - async", async function (assert) { + const store = this.owner.lookup('service:store'); + + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + messages: { + data: [ + { id: '1', - type: 'user', + type: 'message', }, - }, + ], }, }, - }); - message2 = store.push({ - data: { - id: '2', - type: 'message', - attributes: { - title: 'EmberConf is gonna be better', + }, + }); + const message = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, }, }, - }); + }, }); - run(function () { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '2', - type: 'message', - }, - ], - }, + const message2 = store.push({ + data: { + id: '2', + type: 'message', + attributes: { + title: 'EmberConf is gonna be better', + }, + }, + }); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + messages: { + data: [ + { + id: '2', + type: 'message', + }, + ], }, }, - }); + }, + }); + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, null, 'User was removed correctly'); }); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'User was removed correctly'); - }); - message2.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, user, 'User was set on the second message'); - }); + await message2.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, user, 'User was set on the second message'); }); }); test("Fetching the hasMany that doesn't contain the belongsTo, sets the belongsTo to null - sync", function (assert) { - let store = this.owner.lookup('service:store'); - - let account1; - let account2; - let user; + const store = this.owner.lookup('service:store'); - run(function () { - // tell the store user:1 has account:1 - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [{ id: '1', type: 'account' }], - }, + // tell the store user:1 has account:1 + const user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [{ id: '1', type: 'account' }], }, }, - }); + }, + }); - // tell the store account:1 has user:1 - account1 = store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, - relationships: { - user: { - data: { id: '1', type: 'user' }, - }, + // tell the store account:1 has user:1 + const account1 = store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + relationships: { + user: { + data: { id: '1', type: 'user' }, }, }, - }); + }, + }); - // tell the store account:2 has no user - account2 = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'awesome', - }, + // tell the store account:2 has no user + const account2 = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'awesome', }, - }); + }, + }); - // tell the store user:1 has account:2 and not account:1 - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [{ id: '2', type: 'account' }], - }, + // tell the store user:1 has account:2 and not account:1 + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [{ id: '2', type: 'account' }], }, }, - }); + }, }); - run(function () { - assert.strictEqual(account1.user, null, 'User was removed correctly'); - assert.strictEqual(account2.user, user, 'User was added correctly'); - }); + assert.strictEqual(account1.user, null, 'User was removed correctly'); + assert.strictEqual(account2.user, user, 'User was added correctly'); }); - test('Fetching the hasMany side where the hasMany is undefined does not change the belongsTo side - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching the hasMany side where the hasMany is undefined does not change the belongsTo side - async', async function (assert) { + const store = this.owner.lookup('service:store'); var message, user; - run(function () { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - ], - }, - }, + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { + relationships: { + messages: { + data: [ + { id: '1', - type: 'user', + type: 'message', }, - }, + ], }, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', + }, + }); + message = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, }, }, - }); + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, }); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, user, 'User was not removed'); - }); + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, user, 'User was not removed'); }); }); test('Fetching the hasMany side where the hasMany is undefined does not change the belongsTo side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var account, user; - run(function () { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, - relationships: { - user: { - data: { + relationships: { + accounts: { + data: [ + { id: '1', - type: 'user', + type: 'account', }, - }, + ], }, }, - }); - store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'awesome', + }, + }); + account = store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, }, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + }, + }); + store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'awesome', + }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); + }, }); - run(function () { - assert.strictEqual(account.user, user, 'User was not removed'); - }); + assert.strictEqual(account.user, user, 'User was not removed'); }); /* @@ -784,10 +721,9 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f */ test('Pushing to the hasMany reflects the change on the belongsTo side - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user, message2; - user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -815,7 +751,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - message2 = store.push({ + const message2 = store.push({ data: { id: '2', type: 'message', @@ -834,66 +770,64 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); test('Pushing to the hasMany reflects the change on the belongsTo side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, account2; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, - relationships: { - user: { - data: { + relationships: { + accounts: { + data: [ + { id: '1', - type: 'user', + type: 'account', }, + ], + }, + }, + }, + }); + store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); + }, + }); - account2 = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'awesome', - }, + account2 = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'awesome', }, - }); - user.accounts.push(account2); + }, }); + user.accounts.push(account2); assert.strictEqual(account2.user, user, 'user got set correctly'); }); test('Removing from the hasMany side reflects the change on the belongsTo side - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user = store.push({ + const user = store.push({ data: { id: '1', type: 'user', @@ -912,7 +846,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - let message = store.push({ + const message = store.push({ data: { id: '1', type: 'message', @@ -929,12 +863,12 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); test('Removing from the hasMany side reflects the change on the belongsTo side - sync', function (assert) { - let store = this.owner.lookup('service:store'); - let user = store.push({ + const store = this.owner.lookup('service:store'); + const user = store.push({ data: { id: '1', type: 'user', - attirbutes: { + attributes: { name: 'Stanley', }, relationships: { @@ -949,11 +883,11 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - let account = store.push({ + const account = store.push({ data: { id: '1', type: 'account', - attirbutes: { + attributes: { state: 'great', }, relationships: { @@ -974,7 +908,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f test('Pushing to the hasMany side keeps the oneToMany invariant on the belongsTo side - async', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, user2, message; user = store.push({ @@ -1018,12 +952,12 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f await user2.messages.then(async function (fetchedMessages) { fetchedMessages.push(message); - let p1 = message.user.then(function (fetchedUser) { + const p1 = message.user.then(function (fetchedUser) { assert.strictEqual(fetchedUser, user2, 'user got set correctly'); }); - let p2 = user.messages.then(function (newFetchedMessages) { - assert.strictEqual(get(newFetchedMessages, 'length'), 0, 'message got removed from the old messages hasMany'); + const p2 = user.messages.then(function (newFetchedMessages) { + assert.strictEqual(newFetchedMessages.length, 0, 'message got removed from the old messages hasMany'); }); await Promise.allSettled([p1, p2]); @@ -1031,279 +965,261 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }); test('Pushing to the hasMany side keeps the oneToMany invariant - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, user2, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - user2 = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Stanley', + relationships: { + accounts: { + data: [ + { + id: '1', + type: 'account', + }, + ], }, }, - }); - account = store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, + }, + }); + user2 = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - user2.accounts.push(account); + }, }); + account = store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + }, + }); + user2.accounts.push(account); assert.strictEqual(account.user, user2, 'user got set correctly'); assert.strictEqual(user.accounts.length, 0, 'the account got removed correctly'); assert.strictEqual(user2.accounts.length, 1, 'the account got pushed correctly'); }); - test('Setting the belongsTo side keeps the oneToMany invariant on the hasMany- async', function (assert) { + test('Setting the belongsTo side keeps the oneToMany invariant on the hasMany- async', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, user2, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - ], - }, - }, - }, - }); - user2 = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Tomhuda', - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { + relationships: { + messages: { + data: [ + { id: '1', - type: 'user', + type: 'message', }, + ], + }, + }, + }, + }); + user2 = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Tomhuda', + }, + }, + }); + message = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); - message.set('user', user2); + }, }); + message.set('user', user2); - run(function () { - user.messages.then(function (fetchedMessages) { - assert.strictEqual(get(fetchedMessages, 'length'), 0, 'message got removed from the first user correctly'); - }); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 0, 'message got removed from the first user correctly'); }); - run(function () { - user2.messages.then(function (fetchedMessages) { - assert.strictEqual(get(fetchedMessages, 'length'), 1, 'message got added to the second user correctly'); - }); + await user2.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 1, 'message got added to the second user correctly'); }); }); test('Setting the belongsTo side keeps the oneToMany invariant on the hasMany- sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, user2, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, - }, - }); - user2 = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Stanley', - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, - relationships: { - user: { - data: { + relationships: { + accounts: { + data: [ + { id: '1', - type: 'user', + type: 'account', }, + ], + }, + }, + }, + }); + user2 = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, + }); + account = store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); - account.set('user', user2); + }, }); + account.set('user', user2); assert.strictEqual(account.user, user2, 'user got set correctly'); assert.strictEqual(user.accounts.length, 0, 'the account got removed correctly'); assert.strictEqual(user2.accounts.length, 1, 'the account got pushed correctly'); }); - test('Setting the belongsTo side to null removes the record from the hasMany side - async', function (assert) { + test('Setting the belongsTo side to null removes the record from the hasMany side - async', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '1', - type: 'message', - }, - ], - }, - }, - }, - }); - message = store.push({ - data: { - id: '1', - type: 'message', - attributes: { - title: 'EmberFest was great', - }, - relationships: { - user: { - data: { + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + messages: { + data: [ + { id: '1', - type: 'user', + type: 'message', }, - }, + ], }, }, - }); - message.set('user', null); + }, }); - run(function () { - user.messages.then(function (fetchedMessages) { - assert.strictEqual(get(fetchedMessages, 'length'), 0, 'message got removed from the user correctly'); - }); + message = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, }); + message.set('user', null); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'user got set to null correctly'); - }); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 0, 'message got removed from the user correctly'); + }); + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, null, 'user got set to null correctly'); }); }); test('Setting the belongsTo side to null removes the record from the hasMany side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '1', - type: 'account', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.push({ - data: { - id: '1', - type: 'account', - attributes: { - state: 'great', - }, - relationships: { - user: { - data: { + relationships: { + accounts: { + data: [ + { id: '1', - type: 'user', + type: 'account', }, + ], + }, + }, + }, + }); + account = store.push({ + data: { + id: '1', + type: 'account', + attributes: { + state: 'great', + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); - account.set('user', null); + }, }); + account.set('user', null); assert.strictEqual(account.user, null, 'user got set to null correctly'); @@ -1314,184 +1230,166 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f Rollback attributes from deleted state */ - test('Rollbacking attributes of a deleted record works correctly when the hasMany side has been deleted - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Rollbacking attributes of a deleted record works correctly when the hasMany side has been deleted - async', async function (assert) { + const store = this.owner.lookup('service:store'); var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '2', - type: 'message', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '2', - type: 'message', - attributes: { - title: 'EmberFest was great', + relationships: { + messages: { + data: [ + { + id: '2', + type: 'message', + }, + ], }, }, - }); + }, + }); + message = store.push({ + data: { + id: '2', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + }, }); - run(function () { - message.deleteRecord(); - message.rollbackAttributes(); + + message.deleteRecord(); + message.rollbackAttributes(); + + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, user, 'Message still has the user'); }); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, user, 'Message still has the user'); - }); - user.messages.then(function (fetchedMessages) { - assert.strictEqual(fetchedMessages.at(0), message, 'User has the message'); - }); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.at(0), message, 'User has the message'); }); }); test('Rollbacking attributes of a deleted record works correctly when the hasMany side has been deleted - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var account, user; - run(function () { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); - }); - run(function () { - account.deleteRecord(); - account.rollbackAttributes(); - assert.strictEqual(user.accounts.length, 1, 'Accounts are rolled back'); - assert.strictEqual(account.user, user, 'Account still has the user'); + }, }); + account.deleteRecord(); + account.rollbackAttributes(); + assert.strictEqual(user.accounts.length, 1, 'Accounts are rolled back'); + assert.strictEqual(account.user, user, 'Account still has the user'); }); - test('Rollbacking attributes of deleted record works correctly when the belongsTo side has been deleted - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Rollbacking attributes of deleted record works correctly when the belongsTo side has been deleted - async', async function (assert) { + const store = this.owner.lookup('service:store'); var user, message; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - messages: { - data: [ - { - id: '2', - type: 'message', - }, - ], - }, - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - message = store.push({ - data: { - id: '2', - type: 'message', - attributes: { - title: 'EmberFest was great', + relationships: { + messages: { + data: [ + { + id: '2', + type: 'message', + }, + ], }, }, - }); + }, + }); + message = store.push({ + data: { + id: '2', + type: 'message', + attributes: { + title: 'EmberFest was great', + }, + }, }); - run(function () { - user.deleteRecord(); - user.rollbackAttributes(); + user.deleteRecord(); + user.rollbackAttributes(); + await message.user.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, user, 'Message has the user again'); }); - run(function () { - message.user.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, user, 'Message has the user again'); - }); - user.messages.then(function (fetchedMessages) { - assert.strictEqual(fetchedMessages.length, 1, 'User still has the messages'); - }); + await user.messages.then(function (fetchedMessages) { + assert.strictEqual(fetchedMessages.length, 1, 'User still has the messages'); }); }); test('Rollbacking attributes of a deleted record works correctly when the belongsTo side has been deleted - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var account, user; - run(function () { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - accounts: { - data: [ - { - id: '2', - type: 'account', - }, - ], - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + accounts: { + data: [ + { + id: '2', + type: 'account', + }, + ], }, }, - }); - }); - run(function () { - user.deleteRecord(); - user.rollbackAttributes(); - assert.strictEqual(user.accounts.length, 1, 'User still has the accounts'); - assert.strictEqual(account.user, user, 'Account has the user again'); + }, }); + user.deleteRecord(); + user.rollbackAttributes(); + assert.strictEqual(user.accounts.length, 1, 'User still has the accounts'); + assert.strictEqual(account.user, user, 'Account has the user again'); }); /* @@ -1499,8 +1397,8 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f */ test('Rollbacking attributes of a created record works correctly when the hasMany side has been created - async', async function (assert) { - let store = this.owner.lookup('service:store'); - let user = store.push({ + const store = this.owner.lookup('service:store'); + const user = store.push({ data: { id: '1', type: 'user', @@ -1509,46 +1407,44 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - let message = store.createRecord('message', { + const message = store.createRecord('message', { user: user, }); message.rollbackAttributes(); - let fetchedUser = await message.user; + const fetchedUser = await message.user; assert.strictEqual(fetchedUser, null, 'Message does not have the user anymore'); - let fetchedMessages = await user.messages; + const fetchedMessages = await user.messages; assert.strictEqual(fetchedMessages.length, 0, 'User does not have the message anymore'); assert.strictEqual(fetchedMessages.at(0), undefined, "User message can't be accessed"); }); test('Rollbacking attributes of a created record works correctly when the hasMany side has been created - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var user, account; - run(function () { - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - account = store.createRecord('account', { - user: user, - }); + }, }); - run(account, 'rollbackAttributes'); + account = store.createRecord('account', { + user: user, + }); + account.rollbackAttributes(); assert.strictEqual(user.accounts.length, 0, 'Accounts are rolled back'); assert.strictEqual(account.user, null, 'Account does not have the user anymore'); }); test('Rollbacking attributes of a created record works correctly when the belongsTo side has been created - async', async function (assert) { - let store = this.owner.lookup('service:store'); - let message = store.push({ + const store = this.owner.lookup('service:store'); + const message = store.push({ data: { id: '2', type: 'message', @@ -1557,38 +1453,34 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f }, }, }); - let user = store.createRecord('user'); - let messages = await user.messages; + const user = store.createRecord('user'); + const messages = await user.messages; messages.push(message); user.rollbackAttributes(); - let fetchedUser = await message.user; + const fetchedUser = await message.user; assert.strictEqual(fetchedUser, null, 'Message does not have the user anymore'); - let fetchedMessages = await user.messages; + const fetchedMessages = await user.messages; assert.strictEqual(fetchedMessages.length, 0, 'User does not have the message anymore'); assert.strictEqual(fetchedMessages.at(0), undefined, "User message can't be accessed"); }); test('Rollbacking attributes of a created record works correctly when the belongsTo side has been created - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var account, user; - run(function () { - account = store.push({ - data: { - id: '2', - type: 'account', - attributes: { - state: 'lonely', - }, + account = store.push({ + data: { + id: '2', + type: 'account', + attributes: { + state: 'lonely', }, - }); - user = store.createRecord('user'); - }); - run(function () { - user.accounts.push(account); + }, }); - run(user, 'rollbackAttributes'); + user = store.createRecord('user'); + user.accounts.push(account); + user.rollbackAttributes(); 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'); }); @@ -1597,8 +1489,8 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f 'createRecord updates inverse record array which has observers', { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findAll = () => { return { @@ -1617,7 +1509,7 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f const users = await store.findAll('user'); assert.strictEqual(users.length, 1, 'Exactly 1 user'); - let user = users.at(0); + const user = users.at(0); assert.strictEqual(user.messages.length, 0, 'Record array is initially empty'); // set up an observer @@ -1629,11 +1521,11 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.strictEqual(messages.length, 0, 'we have no messages'); assert.strictEqual(user.messages.length, 0, 'we have no messages'); - let message = store.createRecord('message', { user, title: 'EmberFest was great' }); + 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'); - let messageFromArray = user.messages.objectAt(0); + 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 811d5bc546a..2487e22daa0 100644 --- a/tests/main/tests/integration/relationships/one-to-one-test.js +++ b/tests/main/tests/integration/relationships/one-to-one-test.js @@ -1,8 +1,6 @@ -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -11,6 +9,7 @@ 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); @@ -29,7 +28,7 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }); const ApplicationAdapter = Adapter.extend({ - deleteRecord: () => resolve(), + deleteRecord: () => Promise.resolve(), }); const ApplicationSerializer = class extends JSONAPISerializer {}; @@ -45,176 +44,165 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun Server loading tests */ - test('Relationship is available from both sides even if only loaded from one side - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Relationship is available from both sides even if only loaded from one side - async', async function (assert) { + const store = this.owner.lookup('service:store'); var stanley, stanleysFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', - }, + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', }, }, }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", - }, + }, + }); + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", }, - }); + }, + }); - stanleysFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, stanley, 'User relationship was set up correctly'); - }); + await stanleysFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, stanley, 'User relationship was set up correctly'); }); }); test('Relationship is available from both sides even if only loaded from one side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var job, user; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: true, - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: true, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - job: { - data: { - id: '2', - type: 'job', - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + job: { + data: { + id: '2', + type: 'job', }, }, }, - }); + }, }); assert.strictEqual(job.user, user, 'User relationship was set up correctly'); }); - test('Fetching a belongsTo that is set to null removes the record from a relationship - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Fetching a belongsTo that is set to null removes the record from a relationship - async', async function (assert) { + const store = this.owner.lookup('service:store'); var stanleysFriend; - run(function () { - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", - }, - relationships: { - bestFriend: { - data: { - id: '1', - type: 'user', - }, + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", + }, + relationships: { + bestFriend: { + data: { + id: '1', + type: 'user', }, }, }, - }); - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: null, - }, + }, + }); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: null, }, }, - }); - stanleysFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'User relationship was removed correctly'); - }); + }, + }); + await stanleysFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, null, 'User relationship was removed correctly'); }); }); test('Fetching a belongsTo that is set to null removes the record from a relationship - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var job; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: true, - }, + var job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: true, }, - }); - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - job: { - data: { - id: '2', - type: 'job', - }, + }, + }); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + job: { + data: { + id: '2', + type: 'job', }, }, }, - }); + }, }); - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: true, - }, - relationships: { - user: { - data: null, - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: true, + }, + relationships: { + user: { + data: null, }, }, - }); + }, }); assert.strictEqual(job.user, null, 'User relationship was removed correctly'); }); test('Fetching a belongsTo that is set to a different record, sets the old relationship to null - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user1 = store.push({ + const user1 = store.push({ data: { type: 'user', id: '1', @@ -239,8 +227,8 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun ], }); - let user2 = store.peekRecord('user', '2'); - let user1Friend = await user1.bestFriend; + const user2 = store.peekRecord('user', '2'); + const user1Friend = await user1.bestFriend; assert.strictEqual(user1Friend, user2, '.bestFriend is '); @@ -280,16 +268,16 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun ], }); - let user3 = store.peekRecord('user', '3'); - let user1bestFriend = await user1.bestFriend; - let user2bestFriend = await user2.bestFriend; - let user3bestFriend = await user3.bestFriend; + const user3 = store.peekRecord('user', '3'); + const user1bestFriend = await user1.bestFriend; + const user2bestFriend = await user2.bestFriend; + const user3bestFriend = await user3.bestFriend; assert.strictEqual(user3bestFriend, user2, '.bestFriend is '); assert.strictEqual(user2bestFriend, user3, '.bestFriend is '); assert.strictEqual(user1bestFriend, null, '.bestFriend is null'); - let user1bestFriendState = user1.belongsTo('bestFriend').belongsToRelationship; + const user1bestFriendState = user1.belongsTo('bestFriend').belongsToRelationship; assert.strictEqual(user1bestFriendState.remoteState, null, '.job is canonically empty'); assert.strictEqual(user1bestFriendState.localState, null, '.job is locally empty'); @@ -300,9 +288,9 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }); test('Fetching a belongsTo that is set to a different record, sets the old relationship to null - sync', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let user1 = store.push({ + const user1 = store.push({ data: { type: 'user', id: '1', @@ -327,7 +315,7 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun ], }); - let job1 = store.peekRecord('job', '1'); + const job1 = store.peekRecord('job', '1'); assert.strictEqual(user1.job, job1, '.job is '); @@ -367,13 +355,13 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun ], }); - let user2 = store.peekRecord('user', '2'); + const user2 = store.peekRecord('user', '2'); assert.strictEqual(user2.job, job1, '.job is '); assert.strictEqual(job1.user, user2, '.user is '); assert.strictEqual(user1.job, null, '.job is null'); - let user1JobState = user1.belongsTo('job').belongsToRelationship; + const user1JobState = user1.belongsTo('job').belongsToRelationship; assert.strictEqual(user1JobState.remoteState, null, '.job is canonically empty'); assert.strictEqual(user1JobState.localState, null, '.job is locally empty'); @@ -387,223 +375,201 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun Local edits */ - test('Setting a OneToOne relationship reflects correctly on the other side- async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Setting a OneToOne relationship reflects correctly on the other side- async', async function (assert) { + const store = this.owner.lookup('service:store'); var stanley, stanleysFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", - }, + }, + }); + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", }, - }); + }, }); - run(function () { - stanley.set('bestFriend', stanleysFriend); - stanleysFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, stanley, 'User relationship was updated correctly'); - }); + stanley.set('bestFriend', stanleysFriend); + await stanleysFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, stanley, 'User relationship was updated correctly'); }); }); test('Setting a OneToOne relationship reflects correctly on the other side- sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var job, user; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: true, - }, - }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: true, }, - }); + }, }); - run(function () { - user.set('job', job); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, }); + user.job = job; 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 }, - function (assert) { - let store = this.owner.lookup('service:store'); - - var stanley, stanleysFriend, newFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', - }, - }, - }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, }, }, - }); - newFriend = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'New friend', - }, + }, + }); + const stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", }, - }); + }, }); - run(function () { - newFriend.set('bestFriend', stanleysFriend.bestFriend); - stanley.bestFriend.then(function (fetchedUser) { - assert.strictEqual( - fetchedUser, - newFriend, - `Stanley's bestFriend relationship was updated correctly to newFriend` - ); - }); - newFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual( - fetchedUser, - stanley, - `newFriend's bestFriend relationship was updated correctly to be Stanley` - ); - }); + 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 }, - function (assert) { - let store = this.owner.lookup('service:store'); - - var igor, newFriend; - run(function () { - store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - }, - }); - igor = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: 'Igor', - }, - }, - }); - newFriend = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'New friend', - }, - relationships: { - bestFriend: { - data: { - id: '1', - type: 'user', - }, - }, - }, + async function (assert) { + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', }, - }); + }, }); - run(function () { - newFriend.set('bestFriend', igor.bestFriend); - newFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'User relationship was updated correctly'); - }); + const igor = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Igor', + }, + }, }); - } - ); - - testInDebug("Setting a BelongsTo to a promise that didn't come from a relationship errors out", function (assert) { - let store = this.owner.lookup('service:store'); - - var stanley, igor; - run(function () { - stanley = store.push({ + const newFriend = store.push({ data: { - id: '1', + id: '3', type: 'user', attributes: { - name: 'Stanley', + name: 'New friend', }, relationships: { bestFriend: { data: { - id: '2', + id: '1', type: 'user', }, }, }, }, }); - igor = store.push({ - data: { - id: '3', - type: 'user', - attributes: { - name: 'Igor', + 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'); + + 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', + }, + }, }); - assert.expectAssertion(function () { - run(function () { - stanley.set('bestFriend', resolve(igor)); - }); - }, /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./); + 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( @@ -659,14 +625,14 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun adapter.findRecord = function (store, type, id, snapshot) { if (id === '5') { - return resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); + return Promise.resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); } else if (id === '2') { - return resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); + return Promise.resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); } }; - let stanleyPromise = stanley.bestFriend; - let igorPromise = igor.bestFriend; + const stanleyPromise = stanley.bestFriend; + const igorPromise = igor.bestFriend; await Promise.all([stanleyPromise, igorPromise]); newFriend.bestFriend = stanleyPromise; @@ -677,106 +643,98 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun } ); - test('Setting a OneToOne relationship to null reflects correctly on the other side - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Setting a OneToOne relationship to null reflects correctly on the other side - async', async function (assert) { + const store = this.owner.lookup('service:store'); var stanley, stanleysFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', - }, + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', }, }, }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", - }, - relationships: { - bestFriend: { - data: { - id: '1', - type: 'user', - }, + }, + }); + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", + }, + relationships: { + bestFriend: { + data: { + id: '1', + type: 'user', }, }, }, - }); + }, }); - run(function () { - stanley.set('bestFriend', null); // :( - stanleysFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, null, 'User relationship was removed correctly'); - }); + stanley.bestFriend = null; // :( + await stanleysFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, null, 'User relationship was removed correctly'); }); }); test('Setting a OneToOne relationship to null reflects correctly on the other side - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var job, user; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: false, - }, - relationships: { - user: { - data: { - id: '1', - type: 'user', - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: false, + }, + relationships: { + user: { + data: { + id: '1', + type: 'user', }, }, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - job: { - data: { - id: '2', - type: 'job', - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + job: { + data: { + id: '2', + type: 'job', }, }, }, - }); + }, }); - run(function () { - user.set('job', null); - }); + user.job = null; assert.strictEqual(job.user, null, 'User relationship was removed correctly'); }); test('Setting a belongsTo to a different record, sets the old relationship to null - async', async function (assert) { assert.expect(3); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var stanley, stanleysFriend; stanley = store.push({ @@ -836,54 +794,50 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }); test('Setting a belongsTo to a different record, sets the old relationship to null - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var job, user, newBetterJob; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: false, - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: false, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - job: { - data: { - id: '2', - type: 'job', - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + job: { + data: { + id: '2', + type: 'job', }, }, }, - }); + }, }); assert.strictEqual(job.user, user, 'Job and user initially setup correctly'); - run(function () { - newBetterJob = store.push({ - data: { - id: '3', - type: 'job', - attributes: { - isGood: true, - }, + newBetterJob = store.push({ + data: { + id: '3', + type: 'job', + attributes: { + isGood: true, }, - }); - - newBetterJob.set('user', user); + }, }); + newBetterJob.user = user; + assert.strictEqual(user.job, newBetterJob, 'Job updated correctly'); assert.strictEqual(job.user, null, 'Old relationship nulled out correctly'); assert.strictEqual(newBetterJob.user, user, 'New job setup correctly'); @@ -893,94 +847,84 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun Rollback attributes tests */ - test('Rollbacking attributes of deleted record restores the relationship on both sides - async', function (assert) { - let store = this.owner.lookup('service:store'); + test('Rollbacking attributes of deleted record restores the relationship on both sides - async', async function (assert) { + const store = this.owner.lookup('service:store'); var stanley, stanleysFriend; - run(function () { - stanley = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - bestFriend: { - data: { - id: '2', - type: 'user', - }, + stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', }, }, }, - }); - stanleysFriend = store.push({ - data: { - id: '2', - type: 'user', - attributes: { - name: "Stanley's friend", - }, + }, + }); + stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", }, - }); + }, }); - run(function () { - stanley.deleteRecord(); + stanley.deleteRecord(); + stanley.rollbackAttributes(); + await stanleysFriend.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, stanley, 'Stanley got rollbacked correctly'); }); - run(function () { - stanley.rollbackAttributes(); - stanleysFriend.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, stanley, 'Stanley got rollbacked correctly'); - }); - stanley.bestFriend.then(function (fetchedUser) { - assert.strictEqual(fetchedUser, stanleysFriend, 'Stanleys friend did not get removed'); - }); + await stanley.bestFriend.then(function (fetchedUser) { + assert.strictEqual(fetchedUser, stanleysFriend, 'Stanleys friend did not get removed'); }); }); test('Rollbacking attributes of deleted record restores the relationship on both sides - sync', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var job, user; - run(function () { - job = store.push({ - data: { - id: '2', - type: 'job', - attributes: { - isGood: true, - }, + job = store.push({ + data: { + id: '2', + type: 'job', + attributes: { + isGood: true, }, - }); - user = store.push({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Stanley', - }, - relationships: { - job: { - data: { - id: '2', - type: 'job', - }, + }, + }); + user = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + job: { + data: { + id: '2', + type: 'job', }, }, }, - }); - }); - run(function () { - job.deleteRecord(); - job.rollbackAttributes(); + }, }); + job.deleteRecord(); + job.rollbackAttributes(); assert.strictEqual(user.job, job, 'Job got rollbacked correctly'); assert.strictEqual(job.user, user, 'Job still has the user'); }); test('Rollbacking attributes of created record removes the relationship on both sides - async', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const stanleysFriend = store.push({ data: { @@ -1003,7 +947,7 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }); test('Rollbacking attributes of created record removes the relationship on both sides - sync', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const user = store.push({ data: { 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 d4e5e2922a9..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 @@ -5,10 +5,10 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; 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', @@ -142,7 +142,7 @@ module( user.bestMessage = video; }, DEPRECATE_NON_EXPLICIT_POLYMORPHISM - ? "Assertion Failed: 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." + ? "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 db88aba543b..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 @@ -5,10 +5,10 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; -import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@ember-data/deprecations'; 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', diff --git a/tests/main/tests/integration/relationships/promise-many-array-test.js b/tests/main/tests/integration/relationships/promise-many-array-test.js index d168fb5c8e2..1472d937c2b 100644 --- a/tests/main/tests/integration/relationships/promise-many-array-test.js +++ b/tests/main/tests/integration/relationships/promise-many-array-test.js @@ -7,9 +7,9 @@ import { module } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS } from '@ember-data/deprecations'; 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); @@ -89,7 +89,9 @@ module('PromiseManyArray', (hooks) => { owner.register( 'adapter:application', class extends EmberObject { - findRecord() { + 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', @@ -131,6 +133,15 @@ module('PromiseManyArray', (hooks) => { 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'); diff --git a/tests/main/tests/integration/relationships/relationship-links-test.js b/tests/main/tests/integration/relationships/relationship-links-test.js index 78fefc55b9c..b7f1caa77b4 100644 --- a/tests/main/tests/integration/relationships/relationship-links-test.js +++ b/tests/main/tests/integration/relationships/relationship-links-test.js @@ -1,7 +1,6 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -20,7 +19,7 @@ module('JSON:API links access on relationships', function (hooks) { class ApplicationAdapter extends EmberObject { findRecord() {} findHasMany() { - return resolve({ + return Promise.resolve({ data: [], }); } @@ -98,7 +97,7 @@ module('JSON:API links access on relationships', function (hooks) { class ApplicationAdapter extends EmberObject { findRecord() {} findHasMany() { - return resolve({ + return Promise.resolve({ data: [], }); } @@ -176,7 +175,7 @@ module('JSON:API links access on relationships', function (hooks) { class ApplicationAdapter extends EmberObject { findRecord() {} findHasMany() { - return resolve({ + return Promise.resolve({ data: [], }); } @@ -228,7 +227,7 @@ module('JSON:API links access on relationships', function (hooks) { assert.true(!!links, 'We have a links value on the relationship HasManyReference'); assert.deepEqual(links.related, { href: '/the/related/link' }, 'The related link is correctly available'); - let link = toolsRef.link(); + const link = toolsRef.link(); assert.strictEqual(link, '/the/related/link', 'The related link is unwrapped when accessed directly'); // Test we have access via the ManyArray @@ -242,7 +241,7 @@ module('JSON:API links access on relationships', function (hooks) { class ApplicationAdapter extends EmberObject { findRecord() {} findHasMany() { - return resolve({ + return Promise.resolve({ data: [], links: { self: { href: '/some/other/path' }, diff --git a/tests/main/tests/integration/relationships/reload-error-test.js b/tests/main/tests/integration/relationships/reload-error-test.js index 094bf7a9009..37c2d20ab45 100644 --- a/tests/main/tests/integration/relationships/reload-error-test.js +++ b/tests/main/tests/integration/relationships/reload-error-test.js @@ -1,7 +1,6 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { reject } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -26,7 +25,7 @@ module('Relationships | unloading new records', function (hooks) { return false; } findRecord() { - return reject(new Error(`Bad Request`)); + return Promise.reject(new Error(`Bad Request`)); } } diff --git a/tests/main/tests/integration/relationships/rollback-test.ts b/tests/main/tests/integration/relationships/rollback-test.ts new file mode 100644 index 00000000000..e078d83af18 --- /dev/null +++ b/tests/main/tests/integration/relationships/rollback-test.ts @@ -0,0 +1,709 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; + +class App extends Model { + @attr declare name: string; + + @hasMany('config', { async: false, inverse: 'app' }) declare configs: Config[]; + + @belongsTo('cluster', { async: false, inverse: 'apps' }) declare cluster: Cluster; +} + +class Cluster extends Model { + @attr declare name: string; + + @hasMany('app', { async: false, inverse: 'cluster' }) declare apps: App[]; +} + +class Config extends Model { + @attr declare name: string; + + @belongsTo('app', { async: false, inverse: 'configs' }) declare app: App | null; +} + +module('Integration | Relationships | Rollback', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + + owner.register('model:app', App); + owner.register('model:cluster', Cluster); + owner.register('model:config', Config); + + // setup some initial state: + // 1 app with 3 configs and a cluster + // 1 config with no app + // 2 apps with no configs and the same cluster + const store = owner.lookup('service:store') as Store; + store.push({ + data: [ + { + id: '1', + type: 'app', + attributes: { + name: 'app1', + }, + relationships: { + configs: { + data: [ + { id: '1', type: 'config' }, + { id: '2', type: 'config' }, + { id: '3', type: 'config' }, + ], + }, + cluster: { + data: { id: '1', type: 'cluster' }, + }, + }, + }, + // configs + { + id: '1', + type: 'config', + attributes: { + name: 'config1', + }, + relationships: { + app: { + data: { id: '1', type: 'app' }, + }, + }, + }, + { + id: '2', + type: 'config', + attributes: { + name: 'config2', + }, + relationships: { + app: { + data: { id: '1', type: 'app' }, + }, + }, + }, + { + id: '3', + type: 'config', + attributes: { + name: 'config3', + }, + relationships: { + app: { + data: { id: '1', type: 'app' }, + }, + }, + }, + // config with no app + { + id: '4', + type: 'config', + attributes: { + name: 'config4', + }, + }, + // the cluster + { + id: '1', + type: 'cluster', + attributes: { + name: 'cluster1', + }, + relationships: { + apps: { + data: [ + { id: '1', type: 'app' }, + { id: '2', type: 'app' }, + { id: '3', type: 'app' }, + ], + }, + }, + }, + // apps with no configs + { + id: '2', + type: 'app', + attributes: { + name: 'app2', + }, + relationships: { + cluster: { + data: { id: '1', type: 'cluster' }, + }, + }, + }, + { + id: '3', + type: 'app', + attributes: { + name: 'app3', + }, + relationships: { + cluster: { + data: { id: '1', type: 'cluster' }, + }, + }, + }, + ], + }); + }); + + module('.hasChangedRelationships', function () { + test('it returns false when no changes have occurred', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'no changes have occurred'); + }); + test('it returns true when a hasMany has state added', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '4') as Config; + app.configs.push(config); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state added'); + }); + test('it returns true when a hasMany has state removed', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '2') as Config; + app.configs.splice(app.configs.indexOf(config), 1); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + }); + test('it returns true 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 config = store.peekRecord('config', '1') as Config; + app.configs.splice(app.configs.indexOf(config), 1); + app.configs.push(config); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + }); + test('it returns false when a mutated has-many has returned to its initial state', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '1') as Config; + app.configs.shift(); + app.configs.unshift(config); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + }); + test('it returns false when a belongsTo has no change', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config4Identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + assert.false(store.cache.hasChangedRelationships(config4Identifier), 'a belongsTo has no change (null)'); + + const config1Identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.false(store.cache.hasChangedRelationships(config1Identifier), 'a belongsTo has no change (populated)'); + }); + test('it returns true when a belongsTo has state added', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '4') as Config; + config.app = app; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state added'); + }); + test('it returns true when a belongsTo has state removed', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state removed'); + }); + test('it returns true when a belongsTo has state replaced', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = store.peekRecord('app', '2') as App; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + }); + test('it returns false when state has returned to the initial state', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + config.app = store.peekRecord('app', '1') as App; + assert.false(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + }); + }); + + module('.rollbackRelationships', function () { + test('it returns an empty array when no changes have occurred', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + + const changed = store.cache.rollbackRelationships(appIdentifier); + assert.arrayStrictEquals(changed, [], 'no changes have occurred'); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); + }); + + test('it returns the correct keys when a hasMany has state added', 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 config4 = store.peekRecord('config', '4') as Config; + app.configs.push(config4); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state added'); + assert.arrayStrictEquals(app.configs, [config1, config2, config3, config4], 'hasMany has state added'); + assert.strictEqual(config4.app, app, 'config4 has state added'); + + 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(config4.app, null, 'config4 has rolled back'); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); + }); + + test('it returns the correct keys when a hasMany has state removed', 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; + app.configs.splice(app.configs.indexOf(config2), 1); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + assert.arrayStrictEquals(app.configs, [config1, config3], 'hasMany has state added'); + assert.strictEqual(config2.app, null, 'config2 state cleared'); + + 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; + 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 rolled back'); + assert.strictEqual(config2.app, app, 'config2 has rolled back'); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'hasMany is clean'); + }); + + test('it returns an empty array when a mutated has-many has returned to its initial state', 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; + app.configs.shift(); + app.configs.unshift(config1); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany is correct'); + assert.strictEqual(config2.app, app, 'config2 is clean'); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + + const changed = store.cache.rollbackRelationships(appIdentifier); + assert.arrayStrictEquals(changed, [], 'hasMany is clean'); + assert.arrayStrictEquals(app.configs, [config1, config2, config3], 'hasMany is correct'); + assert.strictEqual(config2.app, app, 'config2 is clean'); + }); + + test('it returns an empty array when a belongsTo has no change', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config4Identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + assert.false(store.cache.hasChangedRelationships(config4Identifier), 'a belongsTo has no change (null)'); + + let changed = store.cache.rollbackRelationships(config4Identifier); + assert.arrayStrictEquals(changed, [], 'relationship was clean'); + + const config1Identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.false(store.cache.hasChangedRelationships(config1Identifier), 'a belongsTo has no change (populated)'); + + changed = store.cache.rollbackRelationships(config1Identifier); + assert.arrayStrictEquals(changed, [], 'relationship was clean'); + }); + + test('it returns the correct keys when a belongsTo has state added', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '4') as Config; + config.app = app; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state added'); + + const changed = store.cache.rollbackRelationships(configIdentifier); + assert.arrayStrictEquals(changed, ['app'], 'belongsTo has rolled back'); + assert.strictEqual(config.app, null, 'belongsTo has rolled back'); + }); + + test('it returns the correct keys when a belongsTo has state removed', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + const app = config.app; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state removed'); + + const changed = store.cache.rollbackRelationships(configIdentifier); + assert.arrayStrictEquals(changed, ['app'], 'belongsTo has rolled back'); + assert.strictEqual(config.app, app, 'belongsTo has rolled back'); + }); + + test('it returns the correct keys when a belongsTo has state replaced', function (assert) { + const store = this.owner.lookup('service:store') as Store; + 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 app = config1.app!; + config1.app = store.peekRecord('app', '2') as App; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + assert.arrayStrictEquals(app.configs, [config2, config3], 'inverse has updated'); + + const changed = store.cache.rollbackRelationships(configIdentifier); + + 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 + // we leave that to the user to do if they want to + assert.arrayStrictEquals(app.configs, [config2, config3, config1], 'inverse has rolled back'); + }); + + test('it returns an empty array when state has returned to the initial state', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + config.app = store.peekRecord('app', '1') as App; + assert.false(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + + const changed = store.cache.rollbackRelationships(configIdentifier); + assert.arrayStrictEquals(changed, [], 'belongsTo has rolled back'); + assert.strictEqual(config.app, store.peekRecord('app', '1') as App, 'belongsTo has rolled back'); + }); + + test('relationship rollback can be repeated', function (assert) { + class Message extends Model { + @attr declare msg: string; + } + class Job extends Model { + @attr declare name: string; + @hasMany('message', { async: false, inverse: null }) declare messages: Message[]; + } + + this.owner.register('model:job', Job); + this.owner.register('model:message', Message); + const store = this.owner.lookup('service:store') as Store; + + const job = store.push({ + data: { + id: '1', + type: 'job', + attributes: { + name: 'First Job', + }, + }, + }) as Job; + + const msg1 = store.push({ + data: { + id: '1', + type: 'message', + attributes: { + msg: 'First Message', + }, + }, + }) as Message; + assert.strictEqual(job.messages.length, 0, 'job has 0 messages'); + const jobIdentifier = recordIdentifierFor(job); + + // add message, assert state, rollback, assert state is clean + job.messages.push(msg1); + assert.strictEqual(job.messages.length, 1, 'job has 1 message'); + + const rollbackResult = store.cache.rollbackRelationships(jobIdentifier); + assert.strictEqual(rollbackResult.length, 1, '1 rollbackRelations'); + assert.strictEqual(job.messages.length, 0, 'job has no message'); + + // repeat the scenario to add a message and rollback + job.messages.push(msg1); + assert.strictEqual(job.messages.length, 1, 'job has 1 message'); + + const rollbackResult2 = store.cache.rollbackRelationships(jobIdentifier); + assert.strictEqual(rollbackResult2.length, 1, '1 rollbackRelations'); + assert.strictEqual(job.messages.length, 0, 'job has no message'); + }); + }); + + module('.changedRelationships', function () { + test('it returns an empty map when no changes have occurred', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'no changes have occurred'); + const changed = store.cache.changedRelationships(appIdentifier); + assert.strictEqual(changed.size, 0, 'no changes have occurred'); + }); + + test('it returns the correct entries when a hasMany has state added', function (assert) { + const store = this.owner.lookup('service:store') as Store; + + function identifier(type: string, id: string): StableRecordIdentifier { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '4') as Config; + app.configs.push(config); + const appIdentifier = identifier('app', '1'); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state added'); + const changed = store.cache.changedRelationships(appIdentifier); + assert.strictEqual(changed.size, 1, 'a hasMany has state added'); + + const change = changed.get('configs'); + assert.ok(change, 'a hasMany has state added'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'collection') { + throw new Error('expected a collection change'); + } + assert.strictEqual(change.kind, 'collection', 'kind is collection'); + assert.strictEqual(change.additions.size, 1, 'one entry added'); + assert.strictEqual(change.removals.size, 0, 'no entries removed'); + assert.false(change.reordered, 'no order changes'); + assert.arrayStrictEquals( + change.remoteState, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3')], + 'original state is present' + ); + assert.arrayStrictEquals( + change.localState, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3'), identifier('config', '4')], + 'config4 was added' + ); + }); + + test('it returns the correct entries when a hasMany has state removed', function (assert) { + const store = this.owner.lookup('service:store') as Store; + + function identifier(type: string, id: string): StableRecordIdentifier { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '2') as Config; + app.configs.splice(app.configs.indexOf(config), 1); + const appIdentifier = identifier('app', '1'); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + const changed = store.cache.changedRelationships(appIdentifier); + assert.strictEqual(changed.size, 1, 'a hasMany has state added'); + + const change = changed.get('configs'); + assert.ok(change, 'a hasMany has state added'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'collection') { + throw new Error('expected a collection change'); + } + assert.strictEqual(change.kind, 'collection', 'kind is collection'); + assert.strictEqual(change.additions.size, 0, 'no entry added'); + assert.strictEqual(change.removals.size, 1, 'one entries removed'); + assert.false(change.reordered, 'no order changes'); + assert.arrayStrictEquals( + change.remoteState, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3')], + 'original state is present' + ); + assert.arrayStrictEquals( + change.localState, + [identifier('config', '1'), identifier('config', '3')], + 'config 2 was removed' + ); + }); + + test('it returns the correct entries when a hasMany has state re-ordered', function (assert) { + const store = this.owner.lookup('service:store') as Store; + + function identifier(type: string, id: string): StableRecordIdentifier { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '1') as Config; + app.configs.splice(app.configs.indexOf(config), 1); + app.configs.push(config); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.true(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state reordered'); + + const changed = store.cache.changedRelationships(appIdentifier); + assert.strictEqual(changed.size, 1, 'a hasMany has state added'); + + const change = changed.get('configs'); + assert.ok(change, 'a hasMany has state added'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'collection') { + throw new Error('expected a collection change'); + } + assert.strictEqual(change.kind, 'collection', 'kind is collection'); + assert.strictEqual(change.additions.size, 0, 'no entry added'); + assert.strictEqual(change.removals.size, 0, 'no entries removed'); + assert.true(change.reordered, 'we detect the order changes'); + assert.arrayStrictEquals( + change.remoteState, + [identifier('config', '1'), identifier('config', '2'), identifier('config', '3')], + 'original state is present' + ); + assert.arrayStrictEquals( + change.localState, + [identifier('config', '2'), identifier('config', '3'), identifier('config', '1')], + 'config 1 was moved' + ); + }); + + test('it returns empty when a mutated has-many has returned to its initial state', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '1') as Config; + app.configs.shift(); + app.configs.unshift(config); + const appIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }); + assert.false(store.cache.hasChangedRelationships(appIdentifier), 'a hasMany has state removed'); + + const changed = store.cache.changedRelationships(appIdentifier); + assert.strictEqual(changed.size, 0, 'a hasMany has no state added'); + }); + + test('it returns empty when a belongsTo has no change', function (assert) { + const store = this.owner.lookup('service:store') as Store; + + function identifier(type: string, id: string): StableRecordIdentifier { + return store.identifierCache.getOrCreateRecordIdentifier({ type, id }); + } + + const config4Identifier = identifier('config', '4'); + assert.false(store.cache.hasChangedRelationships(config4Identifier), 'a belongsTo has no change (null)'); + let changed = store.cache.changedRelationships(config4Identifier); + assert.strictEqual(changed.size, 0, 'has no diff'); + + const config1Identifier = identifier('config', '1'); + assert.false(store.cache.hasChangedRelationships(config1Identifier), 'a belongsTo has no change (populated)'); + changed = store.cache.changedRelationships(config1Identifier); + assert.strictEqual(changed.size, 0, 'has no diff'); + }); + + test('it returns the correct diff when a belongsTo has state added', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const app = store.peekRecord('app', '1') as App; + const config = store.peekRecord('config', '4') as Config; + config.app = app; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '4' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state added'); + const changed = store.cache.changedRelationships(configIdentifier); + assert.strictEqual(changed.size, 1, 'a belongsTo has state added'); + + const change = changed.get('app'); + assert.ok(change, 'a belongsTo has state added'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'resource') { + throw new Error('expected a resource change'); + } + + assert.strictEqual(change.kind, 'resource', 'kind is resource'); + assert.strictEqual(change.remoteState, null, 'original state is null'); + assert.strictEqual( + change.localState, + store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }), + 'app was added' + ); + }); + + test('it returns the correct diff when a belongsTo has state removed', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state removed'); + + const changed = store.cache.changedRelationships(configIdentifier); + assert.strictEqual(changed.size, 1, 'a belongsTo has a diff'); + + const change = changed.get('app'); + assert.ok(change, 'the diff is present'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'resource') { + throw new Error('expected a resource change'); + } + + assert.strictEqual(change.kind, 'resource', 'kind is resource'); + assert.strictEqual( + change.remoteState, + store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }), + 'remote state is app 1' + ); + assert.strictEqual(change.localState, null, 'new state is null'); + }); + + test('it returns the correct diff when a belongsTo has state replaced', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = store.peekRecord('app', '2') as App; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + + const changed = store.cache.changedRelationships(configIdentifier); + assert.strictEqual(changed.size, 1, 'a belongsTo has a diff'); + + const change = changed.get('app'); + assert.ok(change, 'the diff is present'); + + // ensure the rest of the test runs smoothly with type checking / runtime correctness + if (change?.kind !== 'resource') { + throw new Error('expected a resource change'); + } + + assert.strictEqual(change.kind, 'resource', 'kind is resource'); + assert.strictEqual( + change.remoteState, + store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '1' }), + 'remote state is app 1' + ); + assert.strictEqual( + change.localState, + store.identifierCache.getOrCreateRecordIdentifier({ type: 'app', id: '2' }), + 'new state is app 2' + ); + }); + + test('it returns no diff when state has returned to the initial state', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const config = store.peekRecord('config', '1') as Config; + config.app = null; + const configIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'config', id: '1' }); + assert.true(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + config.app = store.peekRecord('app', '1') as App; + assert.false(store.cache.hasChangedRelationships(configIdentifier), 'a belongsTo has state replaced'); + const changed = store.cache.changedRelationships(configIdentifier); + assert.strictEqual(changed.size, 0, 'we have no diff'); + }); + }); +}); diff --git a/tests/main/tests/integration/relationships/unload-new-record-test.js b/tests/main/tests/integration/relationships/unload-new-record-test.js index 8fe1fb4671d..3a10d957904 100644 --- a/tests/main/tests/integration/relationships/unload-new-record-test.js +++ b/tests/main/tests/integration/relationships/unload-new-record-test.js @@ -228,12 +228,12 @@ module('Relationships | unloading new records', function (hooks) { ['5'], 'Precond: entryNode has the correct asyncEdges' ); - let originalRelatedNode = await newNode.relatedGraph; + const originalRelatedNode = await newNode.relatedGraph; assert.strictEqual(originalRelatedNode, null, 'PreCond: newNode has no relatedGraph yet'); set(newNode, 'relatedGraph', entryNode); - let value = await newNode.relatedGraph; + const value = await newNode.relatedGraph; asyncEdges = await entryNode.asyncEdges; assert.strictEqual(value, entryNode, 'PreCond: We properly set the async belongsTo to the new value'); diff --git a/tests/main/tests/integration/request-state-service-test.ts b/tests/main/tests/integration/request-state-service-test.ts index 3ca899b44c8..b8770174d2b 100644 --- a/tests/main/tests/integration/request-state-service-test.ts +++ b/tests/main/tests/integration/request-state-service-test.ts @@ -1,22 +1,19 @@ import EmberObject from '@ember/object'; import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; import JSONSerializer from '@ember-data/serializer/json'; import type Store from '@ember-data/store'; -import type { DSModel } from '@ember-data/types/q/ds-model'; class Person extends Model { - // TODO fix the typing for naked attrs @attr('string', {}) - name; + declare name: string; @attr('string', {}) - lastName; + declare lastName: string; } module('integration/request-state-service - Request State Service', function (hooks) { @@ -25,7 +22,7 @@ module('integration/request-state-service - Request State Service', function (ho let store: Store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.register('serializer:application', JSONSerializer); store = owner.lookup('service:store') as Store; @@ -34,7 +31,7 @@ module('integration/request-state-service - Request State Service', function (ho test('getPendingRequest and getLastRequest return correct inflight and fulfilled requests', async function (assert) { assert.expect(10); - let normalizedHash = { + const normalizedHash = { data: { type: 'person', id: '1', @@ -47,9 +44,9 @@ module('integration/request-state-service - Request State Service', function (ho included: [], }; - let { owner } = this; + const { owner } = this; - let TestAdapter = EmberObject.extend({ + const TestAdapter = EmberObject.extend({ findRecord() { const personHash = { type: 'person', @@ -76,17 +73,17 @@ module('integration/request-state-service - Request State Service', function (ho store = owner.lookup('service:store') as Store; - let promise = store.findRecord('person', '1'); - let requestService = store.getRequestStateService(); + const promise = store.findRecord('person', '1'); + const requestService = store.getRequestStateService(); // Relying on sequential lids until identifiers land - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); normalizedHash.data.lid = identifier.lid; - let request = requestService.getPendingRequestsForRecord(identifier)[0]; + const request = requestService.getPendingRequestsForRecord(identifier)[0]; assert.strictEqual(request.state, 'pending', 'request is pending'); assert.strictEqual(request.type, 'query', 'request is a query'); - let requestOp = { + const requestOp = { op: 'findRecord', recordIdentifier: identifier, options: { @@ -95,9 +92,9 @@ module('integration/request-state-service - Request State Service', function (ho }; assert.deepEqual(request.request.data[0], requestOp, 'request op is correct'); - let person = (await promise) as DSModel; - let lastRequest = requestService.getLastRequestForRecord(identifier); - let requestStateResult = { + const person = (await promise) as Model; + const lastRequest = requestService.getLastRequestForRecord(identifier); + const requestStateResult = { type: 'query' as const, state: 'fulfilled' as const, request: { data: [requestOp] }, @@ -106,12 +103,12 @@ module('integration/request-state-service - Request State Service', function (ho assert.deepEqual(lastRequest, requestStateResult, 'request is correct after fulfilling'); assert.deepEqual(requestService.getPendingRequestsForRecord(identifier).length, 0, 'no pending requests remaining'); - let savingPromise = person.save(); - let savingRequest = requestService.getPendingRequestsForRecord(identifier)[0]; + const savingPromise = person.save(); + const savingRequest = requestService.getPendingRequestsForRecord(identifier)[0]; assert.strictEqual(savingRequest.state, 'pending', 'request is pending'); assert.strictEqual(savingRequest.type, 'mutation', 'request is a mutation'); - let savingRequestOp = { + const savingRequestOp = { op: 'saveRecord', recordIdentifier: identifier, options: {}, @@ -119,8 +116,8 @@ module('integration/request-state-service - Request State Service', function (ho assert.deepEqual(savingRequest.request.data[0], savingRequestOp, 'request op is correct'); await savingPromise; - let lastSavingRequest = requestService.getLastRequestForRecord(identifier); - let savingRequestStateResult = { + const lastSavingRequest = requestService.getLastRequestForRecord(identifier); + const savingRequestStateResult = { type: 'mutation' as const, state: 'fulfilled' as const, request: { data: [savingRequestOp] }, @@ -139,7 +136,7 @@ module('integration/request-state-service - Request State Service', function (ho name: 'Scumbag Dale', }; - let normalizedHash = { + const normalizedHash = { data: { type: 'person', id: '1', @@ -152,9 +149,9 @@ module('integration/request-state-service - Request State Service', function (ho included: [], }; - let { owner } = this; + const { owner } = this; - let TestAdapter = EmberObject.extend({ + const TestAdapter = EmberObject.extend({ findRecord() { return Promise.resolve(personHash); }, @@ -175,18 +172,18 @@ module('integration/request-state-service - Request State Service', function (ho store = owner.lookup('service:store') as Store; - let requestService = store.getRequestStateService(); + const requestService = store.getRequestStateService(); // Relying on sequential lids until identifiers land - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); let count = 0; - let requestOp = { + const requestOp = { op: 'findRecord', recordIdentifier: identifier, options: { reload: true, }, }; - let savingRequestOp = { + const savingRequestOp = { op: 'saveRecord', recordIdentifier: identifier, options: {}, @@ -221,7 +218,7 @@ module('integration/request-state-service - Request State Service', function (ho count++; }); - let person = (await store.findRecord('person', '1')) as DSModel; + const person = (await store.findRecord('person', '1')) as Model; await person.save(); assert.strictEqual(count, 4, 'callback called four times'); }); diff --git a/tests/main/tests/integration/serializers/embedded-records-mixin-test.js b/tests/main/tests/integration/serializers/embedded-records-mixin-test.js index 2085abba003..711bef5efd5 100644 --- a/tests/main/tests/integration/serializers/embedded-records-mixin-test.js +++ b/tests/main/tests/integration/serializers/embedded-records-mixin-test.js @@ -49,7 +49,7 @@ module('integration/embedded-records-mixin', function (hooks) { } hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:super-villain', SuperVillain); owner.register('model:home-planet', HomePlanet); @@ -384,11 +384,11 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with embedded objects of same type, but from separate attributes', async function (assert) { - let { owner } = this; + const { owner } = this; class HomePlanetKlass extends Model { @attr('string') name; @hasMany('super-villain', { inverse: 'homePlanet', async: false }) villains; - @hasMany('superVillain', { inverse: null, async: false }) reformedVillains; + @hasMany('super-villain', { inverse: null, async: false }) reformedVillains; } owner.unregister('model:home-planet'); owner.register('model:home-planet', HomePlanetKlass); @@ -497,7 +497,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with multiply-nested belongsTo', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:evil-minion', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -588,13 +588,13 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with polymorphic hasMany and custom primary key', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillainClass extends Model { @attr('string') firstName; @attr('string') lastName; @belongsTo('home-planet', { inverse: 'villains', async: true }) homePlanet; @belongsTo('secret-lab', { async: false, inverse: 'superVillain' }) secretLab; - @hasMany('secretWeapon', { polymorphic: true, async: false, inverse: 'superVillain' }) secretWeapons; + @hasMany('secret-weapon', { polymorphic: true, async: false, inverse: 'superVillain' }) secretWeapons; @hasMany('evil-minion', { async: false, inverse: 'superVillain' }) evilMinions; } @@ -684,12 +684,12 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with polymorphic belongsTo', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillainClass extends Model { @attr('string') firstName; @attr('string') lastName; @belongsTo('home-planet', { inverse: 'villains', async: true }) homePlanet; - @belongsTo('secretLab', { polymorphic: true, async: true, inverse: 'superVillain' }) secretLab; + @belongsTo('secret-lab', { polymorphic: true, async: true, inverse: 'superVillain' }) secretLab; @hasMany('secret-weapon', { async: false, inverse: 'superVillain' }) secretWeapons; @hasMany('evil-minion', { async: false, inverse: 'superVillain' }) evilMinions; } @@ -754,12 +754,12 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with polymorphic belongsTo and custom primary key', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillainClass extends Model { @attr('string') firstName; @attr('string') lastName; @belongsTo('home-planet', { inverse: 'villains', async: true }) homePlanet; - @belongsTo('secretLab', { polymorphic: true, async: true, inverse: 'superVillain' }) secretLab; + @belongsTo('secret-lab', { polymorphic: true, async: true, inverse: 'superVillain' }) secretLab; @hasMany('secret-weapon', { async: false, inverse: 'superVillain' }) secretWeapons; @hasMany('evil-minion', { async: false, inverse: 'superVillain' }) evilMinions; } @@ -833,7 +833,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalize with custom belongsTo primary key', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:evil-minion', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -957,7 +957,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with embedded objects with custom primary key', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:super-villain', RESTSerializer.extend({ @@ -1187,11 +1187,11 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with embedded objects of same type, but from separate attributes', async function (assert) { - let { owner } = this; + const { owner } = this; class HomePlanetClass extends Model { @attr('string') name; @hasMany('super-villain', { inverse: 'homePlanet', async: false }) villains; - @hasMany('superVillain', { async: false, inverse: null }) reformedVillains; + @hasMany('super-villain', { async: false, inverse: null }) reformedVillains; } owner.unregister('model:home-planet'); owner.register('model:home-planet', HomePlanetClass); @@ -1454,14 +1454,14 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('normalizeResponse with polymorphic hasMany', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillainClass extends Model { @attr('string') firstName; @attr('string') lastName; @belongsTo('home-planet', { inverse: 'villains', async: true }) homePlanet; @belongsTo('secret-lab', { async: false, inverse: 'superVillain' }) secretLab; - @hasMany('secretWeapon', { polymorphic: true, async: false, inverse: 'superVillain' }) secretWeapons; + @hasMany('secret-weapon', { polymorphic: true, async: false, inverse: 'superVillain' }) secretWeapons; @hasMany('evil-minion', { async: false, inverse: 'superVillain' }) evilMinions; } @@ -1578,16 +1578,16 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123' }); - let secretLab = store.createRecord('secret-lab', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123' }); + const secretLab = store.createRecord('secret-lab', { minionCapacity: 5000, vicinity: 'California, USA', id: '101', }); - let superVillain = store.createRecord('super-villain', { + const superVillain = store.createRecord('super-villain', { id: '1', firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', homePlanet, secretLab, }); @@ -1606,7 +1606,7 @@ module('integration/embedded-records-mixin', function (hooks) { const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); const expectedOutput = { firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', homePlanet: '123', evilMinions: [ { @@ -1624,7 +1624,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serializing relationships with an embedded and without calls super when not attr not present', async function (assert) { - let { owner } = this; + const { owner } = this; let calledSerializeBelongsTo = false; let calledSerializeHasMany = false; @@ -1636,12 +1636,12 @@ module('integration/embedded-records-mixin', function (hooks) { serializeHasMany(snapshot, json, relationship) { calledSerializeHasMany = true; - let key = relationship.key; - let payloadKey = this.keyForRelationship ? this.keyForRelationship(key, 'hasMany') : key; - let schema = this.store.modelFor(snapshot.modelName); - let relationshipType = schema.determineRelationshipType(relationship, store); + const key = relationship.name; + const payloadKey = this.keyForRelationship ? this.keyForRelationship(key, 'hasMany') : key; + const schema = this.store.modelFor(snapshot.modelName); + const relationshipType = schema.determineRelationshipType(relationship, store); // "manyToOne" not supported in ActiveModelSerializer.prototype.serializeHasMany - let relationshipTypes = ['manyToNone', 'manyToMany', 'manyToOne']; + const relationshipTypes = ['manyToNone', 'manyToMany', 'manyToOne']; if (relationshipTypes.indexOf(relationshipType) > -1) { json[payloadKey] = snapshot.hasMany(key, { ids: true }); } @@ -1661,19 +1661,19 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123', }); - let secretLab = store.createRecord('secret-lab', { + const secretLab = store.createRecord('secret-lab', { minionCapacity: 5000, vicinity: 'California, USA', id: '101', }); - let superVillain = store.createRecord('super-villain', { + const superVillain = store.createRecord('super-villain', { id: '1', firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', homePlanet, secretLab, }); @@ -1692,7 +1692,7 @@ module('integration/embedded-records-mixin', function (hooks) { const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); const expectedOutput = { firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', homePlanet: '123', evilMinions: [ { @@ -1722,7 +1722,7 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123', }); @@ -1766,7 +1766,7 @@ module('integration/embedded-records-mixin', function (hooks) { }, }) ); - let homePlanet = store.createRecord('home-planet', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123', }); @@ -1819,7 +1819,7 @@ module('integration/embedded-records-mixin', function (hooks) { }, }); const serializer = store.serializerFor('home-planet'); - let league = store.peekRecord('home-planet', 123); + const league = store.peekRecord('home-planet', 123); let serializedRestJson; const expectedOutput = { name: 'Villain League', @@ -1843,7 +1843,7 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123', }); @@ -1873,7 +1873,7 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { + const homePlanet = store.createRecord('home-planet', { name: 'Villain League', id: '123', }); @@ -1913,10 +1913,10 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let superVillain = store.createRecord('super-villain', { + const superVillain = store.createRecord('super-villain', { id: '1', firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', }); store.createRecord('evil-minion', { id: '1', @@ -1933,7 +1933,7 @@ module('integration/embedded-records-mixin', function (hooks) { const serializedRestJson = serializer.serialize(superVillain._createSnapshot()); const expectedOutput = { firstName: 'Super', - lastName: 'Villian', + lastName: 'Villain', homePlanet: null, evilMinions: [ { @@ -1949,7 +1949,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize has many relationship using the `ids-and-types` strategy', async function (assert) { - let { owner } = this; + const { owner } = this; class NormalMinion extends Model { @attr('string') name; } @@ -1973,15 +1973,15 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let yellowMinion = store.createRecord('yellow-minion', { + const yellowMinion = store.createRecord('yellow-minion', { id: '1', name: 'Yellowy', }); - let redMinion = store.createRecord('red-minion', { + const redMinion = store.createRecord('red-minion', { id: '1', name: 'Reddy', }); - let commanderVillain = store.createRecord('commander-villain', { + const commanderVillain = store.createRecord('commander-villain', { id: '1', name: 'Jeff', minions: [yellowMinion, redMinion], @@ -2007,7 +2007,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serializing embedded hasMany respects remapped attrs key', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:home-planet', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2026,7 +2026,7 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); + const homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); store.createRecord('super-villain', { firstName: 'Ice', lastName: 'Creature', @@ -2052,7 +2052,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serializing ids hasMany respects remapped attrs key', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:home-planet', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2071,8 +2071,8 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); - let superVillain = store.createRecord('super-villain', { + const homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); + const superVillain = store.createRecord('super-villain', { firstName: 'Ice', lastName: 'Creature', homePlanet, @@ -2100,12 +2100,12 @@ module('integration/embedded-records-mixin', function (hooks) { ); // records with an id, persisted - let secretLab = store.createRecord('secret-lab', { + const secretLab = store.createRecord('secret-lab', { minionCapacity: 5000, vicinity: 'California, USA', id: '101', }); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2132,7 +2132,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize with embedded object (polymorphic belongsTo relationship)', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:super-villain', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2152,7 +2152,7 @@ module('integration/embedded-records-mixin', function (hooks) { owner.unregister('model:super-villain'); owner.register('model:super-villain', SuperVillain); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { id: '1', firstName: 'Tom', lastName: 'Dale', @@ -2191,7 +2191,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize with embedded object (belongsTo relationship) works with different primaryKeys', async function (assert) { - let { owner } = this; + const { owner } = this; owner.register( 'serializer:super-villain', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2211,7 +2211,7 @@ module('integration/embedded-records-mixin', function (hooks) { const superVillainSerializer = store.serializerFor('super-villain'); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2257,7 +2257,7 @@ module('integration/embedded-records-mixin', function (hooks) { const serializer = store.serializerFor('super-villain'); // records without ids, new - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', secretLab: store.createRecord('secret-lab', { @@ -2281,7 +2281,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize with embedded object (polymorphic belongsTo relationship) supports serialize:ids', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillain extends Model { @attr('string') firstName; @attr('string') lastName; @@ -2301,7 +2301,7 @@ module('integration/embedded-records-mixin', function (hooks) { owner.unregister('model:super-villain'); owner.register('model:super-villain', SuperVillain); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2326,7 +2326,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize with embedded object (belongsTo relationship) supports serialize:id', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillain extends Model { @attr('string') firstName; @attr('string') lastName; @@ -2347,7 +2347,7 @@ module('integration/embedded-records-mixin', function (hooks) { owner.unregister('model:super-villain'); owner.register('model:super-villain', SuperVillain); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2373,7 +2373,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serialize with embedded object (belongsTo relationship) supports serialize:id in conjunction with deserialize:records', async function (assert) { - let { owner } = this; + const { owner } = this; class SuperVillain extends Model { @attr('string') firstName; @attr('string') lastName; @@ -2394,7 +2394,7 @@ module('integration/embedded-records-mixin', function (hooks) { owner.unregister('model:super-villain'); owner.register('model:super-villain', SuperVillain); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2434,7 +2434,7 @@ module('integration/embedded-records-mixin', function (hooks) { ); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2458,7 +2458,7 @@ module('integration/embedded-records-mixin', function (hooks) { assert.deepEqual(serializedRestJson, expectedOutput, 'We serialized the belongsTo relationships to IDs'); }); - test('serialize with embedded object (belongsTo relationship) supports serialize:id', async function (assert) { + test('serialize with embedded object (belongsTo relationship) supports serialize:id, v2', async function (assert) { this.owner.register( 'serializer:super-villain', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2469,7 +2469,7 @@ module('integration/embedded-records-mixin', function (hooks) { ); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2493,7 +2493,7 @@ module('integration/embedded-records-mixin', function (hooks) { assert.deepEqual(serializedRestJson, expectedOutput, 'We serialized the belongsTo relationships to IDs'); }); - test('serialize with embedded object (belongsTo relationship) supports serialize:id in conjunction with deserialize:records', async function (assert) { + test('serialize with embedded object (belongsTo relationship) supports serialize:id in conjunction with deserialize:records, v2', async function (assert) { this.owner.register( 'serializer:super-villain', RESTSerializer.extend(EmbeddedRecordsMixin, { @@ -2504,7 +2504,7 @@ module('integration/embedded-records-mixin', function (hooks) { ); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2539,7 +2539,7 @@ module('integration/embedded-records-mixin', function (hooks) { ); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2570,7 +2570,7 @@ module('integration/embedded-records-mixin', function (hooks) { this.owner.register('serializer:super-villain', RESTSerializer.extend(EmbeddedRecordsMixin)); // records with an id, persisted - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2604,7 +2604,7 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let tom = store.createRecord('super-villain', { + const tom = store.createRecord('super-villain', { firstName: 'Tom', lastName: 'Dale', id: '1', @@ -2628,7 +2628,7 @@ module('integration/embedded-records-mixin', function (hooks) { }); test('serializing belongsTo correctly removes embedded foreign key', async function (assert) { - let { owner } = this; + const { owner } = this; class SecretWeaponClass extends Model { @attr('string') name; } @@ -2650,8 +2650,8 @@ module('integration/embedded-records-mixin', function (hooks) { owner.register('model:secret-weapon', SecretWeaponClass); owner.register('model:evil-minion', EvilMinionClass); - let secretWeapon = store.createRecord('secret-weapon', { name: 'Secret Weapon' }); - let evilMinion = store.createRecord('evil-minion', { + const secretWeapon = store.createRecord('secret-weapon', { name: 'Secret Weapon' }); + const evilMinion = store.createRecord('evil-minion', { name: 'Evil Minion', secretWeapon, }); @@ -2682,8 +2682,8 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); - let superVillain = store.createRecord('super-villain', { + const homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); + const superVillain = store.createRecord('super-villain', { firstName: 'Ice', lastName: 'Creature', homePlanet, @@ -2713,8 +2713,8 @@ module('integration/embedded-records-mixin', function (hooks) { }) ); - let homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); - let superVillain = store.createRecord('super-villain', { + const homePlanet = store.createRecord('home-planet', { name: 'Hoth' }); + const superVillain = store.createRecord('super-villain', { firstName: 'Ice', lastName: 'Creature', homePlanet, diff --git a/tests/main/tests/integration/serializers/json-api-serializer-test.js b/tests/main/tests/integration/serializers/json-api-serializer-test.js index 90ae696e2c1..e2a644f3d10 100644 --- a/tests/main/tests/integration/serializers/json-api-serializer-test.js +++ b/tests/main/tests/integration/serializers/json-api-serializer-test.js @@ -1,6 +1,3 @@ -import { get } from '@ember/object'; -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; @@ -60,8 +57,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }); test('Calling pushPayload works', async function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.pushPayload(store, { data: { @@ -120,8 +117,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }); testInDebug('Warns when normalizing an unknown type', function (assert) { - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var documentHash = { data: { @@ -134,15 +131,13 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }; assert.expectWarning(function () { - run(function () { - store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); - }); + store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); }, /Encountered a resource object with type "UnknownType", but no model was found for model name "unknown-type"/); }); testInDebug('Warns when normalizing payload with unknown type included', function (assert) { - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var documentHash = { data: { @@ -170,14 +165,12 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }; assert.expectWarning(function () { - run(function () { - store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); - }); + store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); }, /Encountered a resource object with type "unknown-types", but no model was found for model name "unknown-type"/); }); testInDebug('Warns but does not fail when pushing payload with unknown type included', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var documentHash = { data: { @@ -200,19 +193,17 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }; assert.expectWarning(function () { - run(function () { - store.pushPayload(documentHash); - }); + store.pushPayload(documentHash); }, /Encountered a resource object with type "unknown-types", but no model was found for model name "unknown-type"/); - var user = store.peekRecord('user', 1); - assert.strictEqual(get(user, 'firstName'), 'Yehuda', 'firstName is correct'); + const user = store.peekRecord('user', 1); + assert.strictEqual(user.firstName, 'Yehuda', 'firstName is correct'); }); testInDebug('Errors when pushing payload with unknown type included in relationship', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - var documentHash = { + const documentHash = { data: { type: 'users', id: '1', @@ -229,15 +220,13 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }; assert.expectAssertion(function () { - run(function () { - store.pushPayload(documentHash); - }); - }, /No model was found for 'unknown-type'/); + store.pushPayload(documentHash); + }, "Missing Schema: Encountered a relationship identifier with type 'unknown-type' for the belongsTo relationship 'company' on , Expected an identifier with type 'company'. No schema was found for 'unknown-type'."); }); testInDebug('Warns when normalizing with type missing', function (assert) { - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var documentHash = { data: { @@ -249,9 +238,7 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }; assert.expectAssertion(function () { - run(function () { - store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); - }); + store.serializerFor('user').normalizeResponse(store, User, documentHash, '1', 'findRecord'); }, /Encountered a resource object with an undefined type/); }); @@ -267,8 +254,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var jsonHash = { data: { @@ -302,6 +289,67 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi assert.deepEqual(user.data.relationships.company.data, { id: '2', type: 'company' }); }); + test('Serializer should preserve lid in payloads', function (assert) { + const store = this.owner.lookup('service:store'); + + var jsonHash = { + data: { + type: 'users', + id: '1', + lid: 'user-1', + attributes: { + firstname_attribute_key: 'Yehuda', + title_attribute_key: 'director', + }, + relationships: { + company: { + data: { type: 'companies', id: '2', lid: 'company-2' }, + }, + handles: { + data: [ + { type: 'github-handles', id: '3' }, + { type: 'twitter-handles', id: '4', lid: 'handle-4' }, + ], + }, + }, + included: [ + { + type: 'companies', + id: '2', + lid: 'company-2', + attributes: { + name: 'Tilde Inc.', + }, + }, + { + type: 'github-handles', + id: '3', + attributes: { + username: 'wycats', + }, + }, + { + type: 'twitter-handles', + id: '4', + lid: 'handle-4', + attributes: { + nickname: '@wycats', + }, + }, + ], + }, + }; + + var user = store.serializerFor('user').normalizeResponse(store, User, jsonHash, '1', 'createRecord'); + + assert.strictEqual(user.data.lid, 'user-1'); + assert.deepEqual(user.data.relationships.company.data, { type: 'company', id: '2', lid: 'company-2' }); + assert.deepEqual(user.data.relationships.handles.data, [ + { type: 'github-handle', id: '3' }, + { type: 'twitter-handle', id: '4', lid: 'handle-4' }, + ]); + }); + test('Serializer should respect the attrs hash when serializing attributes and relationships', function (assert) { this.owner.register( 'serializer:user', @@ -314,25 +362,23 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var company, user; - run(function () { - store.push({ - data: { - type: 'company', - id: '1', - attributes: { - name: 'Tilde Inc.', - }, + store.push({ + data: { + type: 'company', + id: '1', + attributes: { + name: 'Tilde Inc.', }, - }); - company = store.peekRecord('company', 1); - user = store.createRecord('user', { - firstName: 'Yehuda', - title: 'director', - company: company, - }); + }, + }); + company = store.peekRecord('company', 1); + user = store.createRecord('user', { + firstName: 'Yehuda', + title: 'director', + company: company, }); var payload = store.serializerFor('user').serialize(user._createSnapshot()); @@ -352,8 +398,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var jsonHash = { data: { @@ -380,9 +426,9 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); - let project = store.createRecord('project', { 'company-name': 'Tilde Inc.' }); - let payload = store.serializerFor('project').serialize(project._createSnapshot()); + const store = this.owner.lookup('service:store'); + const project = store.createRecord('project', { 'company-name': 'Tilde Inc.' }); + const payload = store.serializerFor('project').serialize(project._createSnapshot()); assert.strictEqual(payload.data.attributes['company_name'], 'Tilde Inc.'); }); @@ -404,8 +450,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi this.owner.register('model:user', User); - let store = this.owner.lookup('service:store'); - let user = store.createRecord('user', { myCustomField: 'value' }); + const store = this.owner.lookup('service:store'); + const user = store.createRecord('user', { myCustomField: 'value' }); this.owner.register( 'transform:custom', @@ -428,32 +474,30 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }); test('a belongsTo relationship that is not set will not be in the relationships key', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - run(function () { - serializer.pushPayload(store, { - data: { - type: 'handles', - id: '1', - }, - }); + serializer.pushPayload(store, { + data: { + type: 'handles', + id: '1', + }, + }); - let handle = store.peekRecord('handle', 1); + const handle = store.peekRecord('handle', 1); - let serialized = handle.serialize({ includeId: true }); - assert.deepEqual(serialized, { - data: { - type: 'handles', - id: '1', - }, - }); + const serialized = handle.serialize({ includeId: true }); + assert.deepEqual(serialized, { + data: { + type: 'handles', + id: '1', + }, }); }); test('a belongsTo relationship that is set to null will show as null in the relationships key', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.pushPayload(store, { data: { @@ -462,10 +506,10 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }, }); - let handle = store.peekRecord('handle', 1); + const handle = store.peekRecord('handle', 1); handle.set('user', null); - let serialized = handle.serialize({ includeId: true }); + const serialized = handle.serialize({ includeId: true }); assert.deepEqual(serialized, { data: { type: 'handles', @@ -480,28 +524,26 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }); test('a belongsTo relationship set to a new record will not show in the relationships key', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - run(function () { - serializer.pushPayload(store, { - data: { - type: 'handles', - id: '1', - }, - }); + serializer.pushPayload(store, { + data: { + type: 'handles', + id: '1', + }, + }); - let handle = store.peekRecord('handle', 1); - let user = store.createRecord('user'); - handle.set('user', user); + const handle = store.peekRecord('handle', 1); + const user = store.createRecord('user'); + handle.set('user', user); - let serialized = handle.serialize({ includeId: true }); - assert.deepEqual(serialized, { - data: { - type: 'handles', - id: '1', - }, - }); + const serialized = handle.serialize({ includeId: true }); + assert.deepEqual(serialized, { + data: { + type: 'handles', + id: '1', + }, }); }); @@ -515,51 +557,49 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(function () { - store.serializerFor('user').pushPayload(store, { - data: { - type: 'users', - id: '1', - relationships: { - handles: { - data: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }, + store.serializerFor('user').pushPayload(store, { + data: { + type: 'users', + id: '1', + relationships: { + handles: { + data: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], }, }, - included: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }); + }, + included: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], + }); - let user = store.peekRecord('user', 1); + const user = store.peekRecord('user', 1); - let serialized = user.serialize({ includeId: true }); + const serialized = user.serialize({ includeId: true }); - assert.deepEqual(serialized, { - data: { - type: 'users', - id: '1', - attributes: { - 'first-name': null, - 'last-name': null, - title: null, - }, - relationships: { - handles: { - data: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }, + assert.deepEqual(serialized, { + data: { + type: 'users', + id: '1', + attributes: { + 'first-name': null, + 'last-name': null, + title: null, + }, + relationships: { + handles: { + data: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], }, }, - }); + }, }); }); @@ -573,52 +613,50 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(function () { - store.serializerFor('user').pushPayload(store, { - data: { - type: 'users', - id: '1', - relationships: { - handles: { - data: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }, + store.serializerFor('user').pushPayload(store, { + data: { + type: 'users', + id: '1', + relationships: { + handles: { + data: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], }, }, - included: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }); + }, + included: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], + }); - let user = store.peekRecord('user', 1); - store.createRecord('handle', { user }); + const user = store.peekRecord('user', 1); + store.createRecord('handle', { user }); - let serialized = user.serialize({ includeId: true }); + const serialized = user.serialize({ includeId: true }); - assert.deepEqual(serialized, { - data: { - type: 'users', - id: '1', - attributes: { - 'first-name': null, - 'last-name': null, - title: null, - }, - relationships: { - handles: { - data: [ - { type: 'handles', id: '1' }, - { type: 'handles', id: '2' }, - ], - }, + assert.deepEqual(serialized, { + data: { + type: 'users', + id: '1', + attributes: { + 'first-name': null, + 'last-name': null, + title: null, + }, + relationships: { + handles: { + data: [ + { type: 'handles', id: '1' }, + { type: 'handles', id: '2' }, + ], }, }, - }); + }, }); }); @@ -632,37 +670,35 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(function () { - store.serializerFor('user').pushPayload(store, { - data: { - type: 'users', - id: '1', - }, - }); + store.serializerFor('user').pushPayload(store, { + data: { + type: 'users', + id: '1', + }, + }); - let user = store.peekRecord('user', 1); - store.createRecord('handle', { user }); + const user = store.peekRecord('user', 1); + store.createRecord('handle', { user }); - let serialized = user.serialize({ includeId: true }); + const serialized = user.serialize({ includeId: true }); - assert.deepEqual(serialized, { - data: { - type: 'users', - id: '1', - attributes: { - 'first-name': null, - 'last-name': null, - title: null, - }, - relationships: { - handles: { - data: [], - }, + assert.deepEqual(serialized, { + data: { + type: 'users', + id: '1', + attributes: { + 'first-name': null, + 'last-name': null, + title: null, + }, + relationships: { + handles: { + data: [], }, }, - }); + }, }); }); @@ -676,7 +712,7 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.serializerFor('user').pushPayload(store, { data: { @@ -697,15 +733,15 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi ], }); - let user = store.peekRecord('user', '1'); - let handle1 = store.peekRecord('handle', '1'); - let handle2 = store.peekRecord('handle', '2'); + const user = store.peekRecord('user', '1'); + const handle1 = store.peekRecord('handle', '1'); + const handle2 = store.peekRecord('handle', '2'); const handles = await user.handles; handles.splice(handles.indexOf(handle1), 1); handles.splice(handles.indexOf(handle2), 1); - let serialized = user.serialize({ includeId: true }); + const serialized = user.serialize({ includeId: true }); assert.deepEqual(serialized, { data: { @@ -740,8 +776,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi }); testInDebug('Asserts when normalized attribute key is not found in payload but original key is', function (assert) { - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var jsonHash = { data: { @@ -761,8 +797,8 @@ module('integration/serializers/json-api-serializer - JSONAPISerializer', functi testInDebug( 'Asserts when normalized relationship key is not found in payload but original key is', function (assert) { - let store = this.owner.lookup('service:store'); - let User = store.modelFor('user'); + const store = this.owner.lookup('service:store'); + const User = store.modelFor('user'); var jsonHash = { data: { diff --git a/tests/main/tests/integration/serializers/json-serializer-test.js b/tests/main/tests/integration/serializers/json-serializer-test.js index a21ab5266bf..76e4b9ef893 100644 --- a/tests/main/tests/integration/serializers/json-serializer-test.js +++ b/tests/main/tests/integration/serializers/json-serializer-test.js @@ -1,19 +1,18 @@ -/* eslint no-prototype-builtins: 'off' */ -// prototype hasOwnProperty has no security issues here because it is not production code - -import { run } from '@ember/runloop'; -import { underscore } from '@ember/string'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { underscore } from '@ember-data/request-utils/string'; import JSONSerializer from '@ember-data/serializer/json'; import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; import Transform from '@ember-data/serializer/transform'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + module('integration/serializer/json - JSONSerializer', function (hooks) { setupTest(hooks); @@ -22,8 +21,8 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }); test("serialize doesn't include ID when includeId is false", function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); class Post extends Model { @attr('string') title; @@ -37,11 +36,11 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let post = store.createRecord('post', { + const post = store.createRecord('post', { title: 'Rails is omakase', comments: [], }); - let json = serializer.serialize(post._createSnapshot(), { includeId: false }); + const json = serializer.serialize(post._createSnapshot(), { includeId: false }); assert.deepEqual(json, { title: 'Rails is omakase', @@ -50,8 +49,8 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }); test("serialize doesn't include relationship if not aware of one", function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); class Post extends Model { @attr('string') title; @@ -65,8 +64,8 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let json = serializer.serialize(post._createSnapshot()); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const json = serializer.serialize(post._createSnapshot()); assert.deepEqual(json, { title: 'Rails is omakase', @@ -74,8 +73,8 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }); test('serialize includes id when includeId is true', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); class Post extends Model { @attr('string') title; @@ -89,13 +88,11 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let post = store.createRecord('post', { title: 'Rails is omakase', comments: [] }); + const post = store.createRecord('post', { title: 'Rails is omakase', comments: [] }); - run(() => { - post.set('id', 'test'); - }); + post.set('id', 'test'); - let json = serializer.serialize(post._createSnapshot(), { includeId: true }); + const json = serializer.serialize(post._createSnapshot(), { includeId: true }); assert.deepEqual(json, { id: 'test', @@ -117,10 +114,10 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const json = {}; serializer.serializeAttribute(post._createSnapshot(), json, 'title', { type: 'string' }); @@ -151,9 +148,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let json = {}; + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const json = {}; store.serializerFor('post').serializeAttribute(post._createSnapshot(), json, 'title', { type: 'string' }); @@ -173,13 +170,13 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const json = {}; - serializer.serializeBelongsTo(comment._createSnapshot(), json, { key: 'post', options: {} }); + serializer.serializeBelongsTo(comment._createSnapshot(), json, comment.constructor.relationshipsByName.get('post')); assert.deepEqual(json, { post: '1' }); }); @@ -197,12 +194,12 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: null }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: null }); + const json = {}; - serializer.serializeBelongsTo(comment._createSnapshot(), json, { key: 'post', options: {} }); + serializer.serializeBelongsTo(comment._createSnapshot(), json, comment.constructor.relationshipsByName.get('post')); assert.deepEqual( json, @@ -226,12 +223,12 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: null }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: null }); + const json = {}; - serializer.serializeBelongsTo(comment._createSnapshot(), json, { key: 'post', options: {} }); + serializer.serializeBelongsTo(comment._createSnapshot(), json, comment.constructor.relationshipsByName.get('post')); assert.deepEqual( json, @@ -264,12 +261,14 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); - let json = {}; + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const json = {}; - store.serializerFor('post').serializeBelongsTo(comment._createSnapshot(), json, { key: 'post', options: {} }); + store + .serializerFor('post') + .serializeBelongsTo(comment._createSnapshot(), json, comment.constructor.relationshipsByName.get('post')); assert.deepEqual(json, { POST: '1', @@ -298,21 +297,21 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); - let comment = store.createRecord('comment', { + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post, id: '1', }); - run(function () { - post.comments.push(comment); - }); + post.comments.push(comment); - let json = {}; + const json = {}; - store.serializerFor('post').serializeHasMany(post._createSnapshot(), json, { key: 'comments', options: {} }); + store + .serializerFor('post') + .serializeHasMany(post._createSnapshot(), json, post.constructor.relationshipsByName.get('comments')); assert.deepEqual(json, { COMMENTS: ['1'], @@ -332,24 +331,24 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let post = run(() => - store.push({ - data: { - id: '1', - type: 'post', - attributes: { - title: 'Rails is omakase', - }, + const post = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + title: 'Rails is omakase', }, - }) - ); - let json = {}; + }, + }); + const json = {}; - store.serializerFor('post').serializeHasMany(post._createSnapshot(), json, { key: 'comments', options: {} }); + store + .serializerFor('post') + .serializeHasMany(post._createSnapshot(), json, post.constructor.relationshipsByName.get('comments')); - assert.notOk(json.hasOwnProperty('comments'), 'Does not add the relationship key to json'); + assert.notOk(hasOwn(json, 'comments'), 'Does not add the relationship key to json'); }); test('shouldSerializeHasMany', function (assert) { @@ -365,13 +364,13 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); store.createRecord('comment', { body: 'Omakase is delicious', post: post, id: '1' }); var snapshot = post._createSnapshot(); var relationship = snapshot.record.relationshipFor('comments'); - var key = relationship.key; + var key = relationship.name; var shouldSerialize = store.serializerFor('post').shouldSerializeHasMany(snapshot, key, relationship); @@ -391,10 +390,10 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('model:post', Post); this.owner.register('model:comment', Comment); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let post = store.createRecord('post', { title: 'Rails is omakase', comments: [] }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const post = store.createRecord('post', { title: 'Rails is omakase', comments: [] }); + const json = {}; serializer.serializeIntoHash(json, store.modelFor('post'), post._createSnapshot()); @@ -411,7 +410,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { } class Comment extends Model { @attr('string') body; - @belongsTo('post', { inverse: null, async: true }) post; + @belongsTo('post', { inverse: null, async: true, polymorphic: true }) post; } this.owner.register('model:post', Post); @@ -423,22 +422,22 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { 'serializer:comment', JSONSerializer.extend({ serializePolymorphicType(record, json, relationship) { - let key = relationship.key; - let belongsTo = record.belongsTo(key); - json[relationship.key + 'TYPE'] = belongsTo.modelName; + const key = relationship.name; + const belongsTo = record.belongsTo(key); + json[relationship.name + 'TYPE'] = belongsTo.modelName; assert.ok(true, 'serializePolymorphicType is called when serialize a polymorphic belongsTo'); }, }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); store .serializerFor('comment') - .serializeBelongsTo(comment._createSnapshot(), {}, { key: 'post', options: { polymorphic: true } }); + .serializeBelongsTo(comment._createSnapshot(), {}, comment.constructor.relationshipsByName.get('post')); }); test('serializePolymorphicType async', function (assert) { @@ -449,7 +448,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { } class Comment extends Model { @attr('string') body; - @belongsTo('post', { inverse: null, async: true }) post; + @belongsTo('post', { inverse: null, async: true, polymorphic: true }) post; } this.owner.register('model:post', Post); @@ -464,13 +463,13 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', id: '1' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); store .serializerFor('comment') - .serializeBelongsTo(comment._createSnapshot(), {}, { key: 'post', options: { async: true, polymorphic: true } }); + .serializeBelongsTo(comment._createSnapshot(), {}, comment.constructor.relationshipsByName.get('post')); }); test('normalizeResponse normalizes each record in the array', function (assert) { @@ -502,11 +501,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(function () { - store.serializerFor('post').normalizeResponse(store, store.modelFor('post'), posts, null, 'findAll'); - }); + store.serializerFor('post').normalizeResponse(store, store.modelFor('post'), posts, null, 'findAll'); assert.strictEqual(postNormalizeCount, 2, 'two posts are normalized'); }); @@ -540,7 +537,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { my_comments: [1, 2], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -574,7 +571,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { author_name_key: 'DHH', }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') @@ -607,24 +604,22 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let parentPost = run(() => - store.push({ - data: { - type: 'post', - id: '2', - attributes: { - title: 'Rails is omakase', - }, + const parentPost = store.push({ + data: { + type: 'post', + id: '2', + attributes: { + title: 'Rails is omakase', }, - }) - ); - let post = store.createRecord('post', { + }, + }); + const post = store.createRecord('post', { title: 'Rails is omakase', parentPost: parentPost, }); - let payload = store.serializerFor('post').serialize(post._createSnapshot()); + const payload = store.serializerFor('post').serialize(post._createSnapshot()); assert.strictEqual(payload.title_payload_key, 'Rails is omakase'); assert.strictEqual(payload.my_parent, '2'); @@ -661,7 +656,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var Parent = store.modelFor('parent'); var payload = store.serializerFor('parent').normalizeResponse(store, Parent, jsonHash, '1', 'findRecord'); assert.deepEqual(payload.included, [ @@ -708,7 +703,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var Parent = store.modelFor('parent'); var payload = store.serializerFor('parent').normalizeResponse(store, Parent, jsonHash, '1', 'findRecord'); assert.deepEqual(payload.included, [ @@ -751,12 +746,12 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let payload = store.serializerFor('post').serialize(post._createSnapshot()); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const payload = store.serializerFor('post').serialize(post._createSnapshot()); - assert.notOk(payload.hasOwnProperty('title'), 'Does not add the key to instance'); - assert.notOk(payload.hasOwnProperty('[object Object]'), 'Does not add some random key like [object Object]'); + assert.notOk(hasOwn(payload, 'title'), 'Does not add the key to instance'); + assert.notOk(hasOwn(payload, '[object Object]'), 'Does not add some random key like [object Object]'); }); test('Serializer respects `serialize: false` on the attrs hash for a `hasMany` property', function (assert) { @@ -782,15 +777,15 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); store.createRecord('comment', { body: 'Omakase is delicious', post: post }); var serializer = store.serializerFor('post'); var serializedProperty = serializer.keyForRelationship('comments', 'hasMany'); var payload = serializer.serialize(post._createSnapshot()); - assert.notOk(payload.hasOwnProperty(serializedProperty), 'Does not add the key to instance'); + assert.notOk(hasOwn(payload, serializedProperty), 'Does not add the key to instance'); }); test('Serializer respects `serialize: false` on the attrs hash for a `belongsTo` property', function (assert) { @@ -816,18 +811,18 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); var serializer = store.serializerFor('comment'); var serializedProperty = serializer.keyForRelationship('post', 'belongsTo'); var payload = serializer.serialize(comment._createSnapshot()); - assert.notOk(payload.hasOwnProperty(serializedProperty), 'Does not add the key to instance'); + assert.notOk(hasOwn(payload, serializedProperty), 'Does not add the key to instance'); }); - test('Serializer respects `serialize: false` on the attrs hash for a `hasMany` property', function (assert) { + test('Serializer respects `serialize: false` on the attrs hash for a `hasMany` property, v2', function (assert) { assert.expect(1); class Post extends Model { @attr('string') title; @@ -850,18 +845,18 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); store.createRecord('comment', { body: 'Omakase is delicious', post: post }); var serializer = store.serializerFor('post'); var serializedProperty = serializer.keyForRelationship('comments', 'hasMany'); var payload = serializer.serialize(post._createSnapshot()); - assert.notOk(payload.hasOwnProperty(serializedProperty), 'Does not add the key to instance'); + assert.notOk(hasOwn(payload, serializedProperty), 'Does not add the key to instance'); }); - test('Serializer respects `serialize: false` on the attrs hash for a `belongsTo` property', function (assert) { + test('Serializer respects `serialize: false` on the attrs hash for a `belongsTo` property, v2', function (assert) { assert.expect(1); class Post extends Model { @attr('string') title; @@ -884,15 +879,15 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); var serializer = store.serializerFor('comment'); var serializedProperty = serializer.keyForRelationship('post', 'belongsTo'); var payload = serializer.serialize(comment._createSnapshot()); - assert.notOk(payload.hasOwnProperty(serializedProperty), 'Does not add the key to instance'); + assert.notOk(hasOwn(payload, serializedProperty), 'Does not add the key to instance'); }); test('Serializer respects `serialize: true` on the attrs hash for a `hasMany` property', async function (assert) { @@ -918,9 +913,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); const comments = await post.comments; comments.push(comment); @@ -929,7 +924,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { const serializedProperty = serializer.keyForRelationship('comments', 'hasMany'); const payload = serializer.serialize(post._createSnapshot()); - assert.ok(payload.hasOwnProperty(serializedProperty), 'Add the key to instance'); + assert.ok(hasOwn(payload, serializedProperty), 'Add the key to instance'); }); test('Serializer respects `serialize: true` on the attrs hash for a `belongsTo` property', function (assert) { @@ -955,15 +950,15 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Rails is omakase' }); - let comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase' }); + const comment = store.createRecord('comment', { body: 'Omakase is delicious', post: post }); var serializer = store.serializerFor('comment'); var serializedProperty = serializer.keyForRelationship('post', 'belongsTo'); var payload = serializer.serialize(comment._createSnapshot()); - assert.ok(payload.hasOwnProperty(serializedProperty), 'Add the key to instance'); + assert.ok(hasOwn(payload, serializedProperty), 'Add the key to instance'); }); test('Serializer should merge attrs from superclasses', function (assert) { @@ -998,13 +993,13 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Rails is omakase', description: 'Omakase is delicious', anotherString: 'yet another string', }); - let payload = store.serializerFor('post').serialize(post._createSnapshot()); + const payload = store.serializerFor('post').serialize(post._createSnapshot()); assert.strictEqual(payload.title_payload_key, 'Rails is omakase'); assert.strictEqual(payload.description_payload_key, 'Omakase is delicious'); @@ -1034,9 +1029,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let jsonHash = { _ID_: 1, title: 'Rails is omakase' }; - let store = this.owner.lookup('service:store'); - let post = store + const jsonHash = { _ID_: 1, title: 'Rails is omakase' }; + const store = this.owner.lookup('service:store'); + const post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -1065,9 +1060,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { id: '1', title: 'Rails is omakase' }); - let payload = store.serializerFor('post').serialize(post._createSnapshot(), { includeId: true }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { id: '1', title: 'Rails is omakase' }); + const payload = store.serializerFor('post').serialize(post._createSnapshot(), { includeId: true }); assert.strictEqual(payload._ID_, '1'); }); @@ -1095,9 +1090,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let jsonHash = { id: '1', TITLE: 'Rails is omakase' }; - let store = this.owner.lookup('service:store'); - let post = store.serializerFor('post').normalize(store.modelFor('post'), jsonHash); + const jsonHash = { id: '1', TITLE: 'Rails is omakase' }; + const store = this.owner.lookup('service:store'); + const post = store.serializerFor('post').normalize(store.modelFor('post'), jsonHash); assert.strictEqual(post.data.id, '1'); assert.strictEqual(post.data.attributes.title, 'Rails is omakase'); @@ -1126,9 +1121,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let jsonHash = { id: '1', title: 'Rails is omakase', COMMENTS: ['1'] }; - let store = this.owner.lookup('service:store'); - let post = store.serializerFor('post').normalize(store.modelFor('post'), jsonHash); + const jsonHash = { id: '1', title: 'Rails is omakase', COMMENTS: ['1'] }; + const store = this.owner.lookup('service:store'); + const post = store.serializerFor('post').normalize(store.modelFor('post'), jsonHash); assert.deepEqual(post.data.relationships.comments.data, [{ id: '1', type: 'comment' }]); }); @@ -1167,7 +1162,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var normalizedPayload = store.serializerFor('post').normalize(store.modelFor('post'), { id: '1', @@ -1217,20 +1212,19 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { 'serializer:favorite', JSONSerializer.extend({ serializePolymorphicType(snapshot, json, relationship) { - var key = relationship.key; + var key = relationship.name; json[key + 'TYPE'] = snapshot.belongsTo(key).modelName; }, }) ); - let store = this.owner.lookup('service:store'); - let post = store.createRecord('post', { title: 'Kitties are omakase', id: '1' }); - let favorite = store.createRecord('favorite', { post: post, id: '3' }); + const store = this.owner.lookup('service:store'); + const post = store.createRecord('post', { title: 'Kitties are omakase', id: '1' }); + const favorite = store.createRecord('favorite', { post: post, id: '3' }); - store.serializerFor('favorite').serializeBelongsTo(favorite._createSnapshot(), json, { - key: 'post', - options: { polymorphic: true, async: true }, - }); + store + .serializerFor('favorite') + .serializeBelongsTo(favorite._createSnapshot(), json, favorite.constructor.relationshipsByName.get('post')); assert.deepEqual(json, expected, 'returned JSON is correct'); }); @@ -1280,7 +1274,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { ], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var errors = store.serializerFor('post').extractErrors(store, store.modelFor('post'), payload); assert.deepEqual(errors, { @@ -1314,7 +1308,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { this.owner.register('serializer:post', JSONSerializer.extend()); var payload = { - attributeWhichWillBeRemovedinExtractErrors: ['true'], + attributeWhichWillBeRemovedInExtractErrors: ['true'], errors: [ { source: { pointer: 'data/attributes/title' }, @@ -1323,7 +1317,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { ], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var errors = store.serializerFor('post').extractErrors(store, store.modelFor('post'), payload); assert.deepEqual(errors, { title: ['title errors'] }); @@ -1357,7 +1351,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { untouchedSinceNoErrorsSiblingPresent: ['true'], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var errors = store.serializerFor('post').extractErrors(store, store.modelFor('post'), payload); assert.deepEqual(errors, { untouchedSinceNoErrorsSiblingPresent: ['true'] }); @@ -1389,7 +1383,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { 'serializer:post', JSONSerializer.extend({ extractMeta(store, modelClass, payload) { - let meta = this._super(...arguments); + const meta = this._super(...arguments); meta.authors.push('Tomhuda'); return meta; }, @@ -1405,7 +1399,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -1442,7 +1436,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { title: 'Rails is omakase', }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -1480,7 +1474,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { comments: null, }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -1529,7 +1523,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { ], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store .serializerFor('post') .normalizeResponse(store, store.modelFor('post'), jsonHash, '1', 'findRecord'); @@ -1588,7 +1582,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }, ]; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = store.serializerFor('post').normalizeResponse(store, store.modelFor('post'), payload, '1', 'findAll'); assert.deepEqual(post.included, [ @@ -1636,7 +1630,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { title: 'Rails is omakase', }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectWarning(function () { var post = store @@ -1680,9 +1674,9 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('post'); - let post = store.createRecord('post', { custom: 'value' }); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('post'); + const post = store.createRecord('post', { custom: 'value' }); serializer.serialize(post._createSnapshot()); }); @@ -1721,8 +1715,8 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }) ); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('post'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('post'); serializer.normalize(store.modelFor('post'), { custom: 'value', @@ -1769,7 +1763,7 @@ module('integration/serializer/json - JSONSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); var post = this.owner.lookup('serializer:post').normalizeSingleResponse(store, store.modelFor('post'), jsonHash); assert.strictEqual(post.data.attributes.title, 'Rails is omakase'); diff --git a/tests/main/tests/integration/serializers/rest-serializer-test.js b/tests/main/tests/integration/serializers/rest-serializer-test.js index b7f3544bc0d..53c53ea8ce2 100644 --- a/tests/main/tests/integration/serializers/rest-serializer-test.js +++ b/tests/main/tests/integration/serializers/rest-serializer-test.js @@ -1,12 +1,10 @@ -import { camelize, dasherize, decamelize } from '@ember/string'; - import { module, test } from 'qunit'; -import Inflector, { singularize } from 'ember-inflector'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { dasherize, resetToDefaults, singularize, uncountable, underscore } from '@ember-data/request-utils/string'; import JSONSerializer from '@ember-data/serializer/json'; import RESTSerializer from '@ember-data/serializer/rest'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; @@ -67,14 +65,16 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { test('modelNameFromPayloadKey returns always same modelName even for uncountable multi words keys', function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - Inflector.inflector.uncountable('words'); - let expectedModelName = 'multi-words'; + uncountable('words'); + const expectedModelName = 'multi-words'; assert.strictEqual(serializer.modelNameFromPayloadKey('multi_words'), expectedModelName); assert.strictEqual(serializer.modelNameFromPayloadKey('multi-words'), expectedModelName); + + resetToDefaults(); }); test('normalizeResponse should extract meta using extractMeta', function (assert) { @@ -82,21 +82,21 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { 'serializer:home-planet', RESTSerializer.extend({ extractMeta(store, modelClass, payload) { - let meta = this._super(...arguments); + const meta = this._super(...arguments); meta.authors.push('Tomhuda'); return meta; }, }) ); - let jsonHash = { + const jsonHash = { meta: { authors: ['Tomster'] }, home_planets: [{ id: '1', name: 'Umber', superVillains: [1] }], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let json = store + const json = store .serializerFor('home-planet') .normalizeResponse(store, store.modelFor('home-planet'), jsonHash, null, 'findAll'); @@ -106,19 +106,18 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { test('normalizeResponse with custom modelNameFromPayloadKey', function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.modelNameFromPayloadKey = function (root) { - let camelized = camelize(root); - return singularize(camelized); + return root === 'planets' ? 'home-planet' : singularize(dasherize(root)); }; this.owner.register('serializer:home-planet', JSONSerializer.extend()); this.owner.register('serializer:super-villain', JSONSerializer.extend()); - let jsonHash = { - home_planets: [ + const jsonHash = { + planets: [ { id: '1', name: 'Umber', @@ -134,7 +133,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }, ], }; - let array = serializer.normalizeResponse(store, store.modelFor('home-planet'), jsonHash, '1', 'findRecord'); + const array = serializer.normalizeResponse(store, store.modelFor('home-planet'), jsonHash, '1', 'findRecord'); assert.deepEqual(array, { data: { @@ -170,8 +169,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { testInDebug('normalizeResponse with type and custom modelNameFromPayloadKey', function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); let homePlanetNormalizeCount = 0; @@ -189,10 +188,10 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }) ); - let jsonHash = { + const jsonHash = { 'my-custom-type': [{ id: '1', name: 'Umber', type: 'my-custom-type' }], }; - let array = serializer.normalizeResponse(store, store.modelFor('home-planet'), jsonHash, '1', 'findAll'); + const array = serializer.normalizeResponse(store, store.modelFor('home-planet'), jsonHash, '1', 'findAll'); assert.deepEqual(array, { data: [ @@ -211,11 +210,11 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); testInDebug('normalizeResponse warning with custom modelNameFromPayloadKey', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); let homePlanet; - let oldModelNameFromPayloadKey = serializer.modelNameFromPayloadKey; + const oldModelNameFromPayloadKey = serializer.modelNameFromPayloadKey; this.owner.register('serializer:super-villain', JSONSerializer.extend()); this.owner.register('serializer:home-planet', JSONSerializer.extend()); @@ -246,9 +245,9 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { assert.deepEqual(homePlanet.data.relationships.superVillains.data, [{ id: '1', type: 'super-villain' }]); }); - testInDebug('normalizeResponse warning with custom modelNameFromPayloadKey', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + testInDebug('normalizeResponse warning with custom modelNameFromPayloadKey (again)', function (assert) { + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); let homePlanets; this.owner.register('serializer:super-villain', JSONSerializer); @@ -268,11 +267,11 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { // should not warn if a model is found. serializer.modelNameFromPayloadKey = function (root) { - return camelize(singularize(root)); + return root === 'planets' ? 'home-planet' : singularize(dasherize(root)); }; jsonHash = { - home_planets: [{ id: '1', name: 'Umber', superVillains: [1] }], + planets: [{ id: '1', name: 'Umber', superVillains: [1] }], }; assert.expectNoWarning(function () { @@ -285,12 +284,12 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test('serialize polymorphicType', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - let tom = store.createRecord('yellow-minion', { name: 'Alex', id: '124' }); - let ray = store.createRecord('doomsday-device', { evilMinion: tom, name: 'DeathRay' }); - let json = serializer.serialize(ray._createSnapshot()); + const tom = store.createRecord('yellow-minion', { name: 'Alex', id: '124' }); + const ray = store.createRecord('doomsday-device', { evilMinion: tom, name: 'DeathRay' }); + const json = serializer.serialize(ray._createSnapshot()); assert.deepEqual(json, { name: 'DeathRay', @@ -299,22 +298,22 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); }); - test('serialize polymorphicType with decamelized modelName', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + test('serialize polymorphicType with camelCase modelName', function (assert) { + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - let tom = store.createRecord('yellow-minion', { name: 'Alex', id: '124' }); - let ray = store.createRecord('doomsday-device', { evilMinion: tom, name: 'DeathRay' }); - let json = serializer.serialize(ray._createSnapshot()); + const tom = store.createRecord('yellow-minion', { name: 'Alex', id: '124' }); + const ray = store.createRecord('doomsday-device', { evilMinion: tom, name: 'DeathRay' }); + const json = serializer.serialize(ray._createSnapshot()); assert.deepEqual(json['evilMinionType'], 'yellowMinion'); }); test('serialize polymorphic when associated object is null', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let ray = store.createRecord('doomsday-device', { name: 'DeathRay' }); - let json = serializer.serialize(ray._createSnapshot()); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const ray = store.createRecord('doomsday-device', { name: 'DeathRay' }); + const json = serializer.serialize(ray._createSnapshot()); assert.deepEqual(json['evilMinionType'], null); }); @@ -333,13 +332,13 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }) ); - let jsonHash = { + const jsonHash = { evilMinion: { id: '1', name: 'Tom Dale', superVillain: 1 }, superVillains: [{ id: '1', firstName: 'Yehuda', lastName: 'Katz', homePlanet: '1' }], }; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, '1', 'findRecord'); @@ -349,20 +348,19 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { test('normalizeResponse returns null if payload contains null', function (assert) { assert.expect(1); - let jsonHash = { + const jsonHash = { evilMinion: null, }; - let value; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - value = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findRecord'); + const value = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findRecord'); assert.deepEqual(value, { data: null, included: [] }, 'returned value is null'); }); - test('normalizeResponse loads secondary records with correct serializer', function (assert) { + test('normalizeResponse loads secondary records with correct serializer, v2', function (assert) { let superVillainNormalizeCount = 0; this.owner.register('serializer:evil-minion', JSONSerializer); @@ -376,13 +374,13 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }) ); - let jsonHash = { + const jsonHash = { evilMinions: [{ id: '1', name: 'Tom Dale', superVillain: 1 }], superVillains: [{ id: '1', firstName: 'Yehuda', lastName: 'Katz', homePlanet: '1' }], }; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findAll'); @@ -393,23 +391,23 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { this.owner.register('serializer:super-villain', RESTSerializer); this.owner.register('serializer:evil-minion', RESTSerializer); - let evilMinions = []; - // The actual stack size seems to vary based on browser and potenetially hardware and + const evilMinions = []; + // The actual stack size seems to vary based on browser and potentially hardware and // other factors. This number should be large enough to always be an issue. - let stackOverflowSize = 130000; + const stackOverflowSize = 130000; for (let i = 0; i < stackOverflowSize; i++) { evilMinions.push({ id: i.toString(), superVillain: 1 }); } - let jsonHash = { + const jsonHash = { superVillains: [{ id: '1', firstName: 'Yehuda', lastName: 'Katz', homePlanet: '1' }], evilMinions, }; let superVillain; try { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); superVillain = serializer.normalizeResponse(store, store.modelFor('super-villain'), jsonHash, null, 'findAll'); } catch (err) { assert.ok(false, `normalizeResponse could not handle included length of ${stackOverflowSize}`); @@ -428,20 +426,19 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { superVillain: 'is_super_villain', }, keyForAttribute(attr) { - return decamelize(attr); + return underscore(attr); }, }) ); - let jsonHash = { + const jsonHash = { evilMinions: [{ id: '1', name: 'Tom Dale', is_super_villain: 1 }], }; - let array; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - array = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findAll'); + const array = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findAll'); assert.strictEqual(array.data[0].relationships.superVillain.data.id, '1'); }); @@ -454,29 +451,28 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { name: 'full_name', }, keyForAttribute(attr) { - return decamelize(attr); + return underscore(attr); }, }) ); - let jsonHash = { + const jsonHash = { evilMinions: [{ id: '1', full_name: 'Tom Dale' }], }; - let array; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - array = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findAll'); + const array = serializer.normalizeResponse(store, store.modelFor('evil-minion'), jsonHash, null, 'findAll'); assert.strictEqual(array.data[0].attributes.name, 'Tom Dale'); }); test('serializeIntoHash', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); - let league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); - let json = {}; + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); + const league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); + const json = {}; serializer.serializeIntoHash(json, store.modelFor('home-planet'), league._createSnapshot()); @@ -487,12 +483,12 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); }); - test('serializeIntoHash with decamelized modelName', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + test('serializeIntoHash with underscored modelName', function (assert) { + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - let league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); - let json = {}; + const league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); + const json = {}; serializer.serializeIntoHash(json, store.modelFor('home-planet'), league._createSnapshot()); @@ -504,54 +500,56 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test('serializeBelongsTo with async polymorphic', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - let json = {}; - let expected = { evilMinion: '1', evilMinionType: 'evilMinion' }; - let evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Tomster' }); - let doomsdayDevice = store.createRecord('doomsday-device', { + const json = {}; + const expected = { evilMinion: '1', evilMinionType: 'evilMinion' }; + const evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Tomster' }); + const doomsdayDevice = store.createRecord('doomsday-device', { id: '2', name: 'Yehuda', evilMinion: evilMinion, }); - serializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { - key: 'evilMinion', - options: { polymorphic: true, async: true }, - }); + serializer.serializeBelongsTo( + doomsdayDevice._createSnapshot(), + json, + doomsdayDevice.constructor.relationshipsByName.get('evilMinion') + ); assert.deepEqual(json, expected, 'returned JSON is correct'); }); test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is serialized', function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - let json = {}; - let expected = { evilMinion: '1', typeForEvilMinion: 'evilMinion' }; + const json = {}; + const expected = { evilMinion: '1', typeForEvilMinion: 'evilMinion' }; serializer.keyForPolymorphicType = function () { return 'typeForEvilMinion'; }; - let evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Tomster' }); - let doomsdayDevice = store.createRecord('doomsday-device', { + const evilMinion = store.createRecord('evil-minion', { id: '1', name: 'Tomster' }); + const doomsdayDevice = store.createRecord('doomsday-device', { id: '2', name: 'Yehuda', evilMinion: evilMinion, }); - serializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { - key: 'evilMinion', - options: { polymorphic: true, async: true }, - }); + serializer.serializeBelongsTo( + doomsdayDevice._createSnapshot(), + json, + doomsdayDevice.constructor.relationshipsByName.get('evilMinion') + ); assert.deepEqual(json, expected, 'returned JSON is correct'); }); test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is looked up for normalization', function (assert) { - let json = { + const json = { doomsdayDevice: { id: '1', evilMinion: '2', @@ -559,7 +557,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }, }; - let expected = { + const expected = { data: { type: 'doomsday-device', id: '1', @@ -576,22 +574,22 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { included: [], }; - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); serializer.keyForPolymorphicType = function () { return 'typeForEvilMinion'; }; - let normalized = serializer.normalizeResponse(store, store.modelFor('doomsday-device'), json, null, 'findRecord'); + const normalized = serializer.normalizeResponse(store, store.modelFor('doomsday-device'), json, null, 'findRecord'); assert.deepEqual(normalized, expected, 'normalized JSON is correct'); }); test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload root key', function (assert) { - let store = this.owner.lookup('service:store'); - let league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); - let json = {}; + const store = this.owner.lookup('service:store'); + const league = store.createRecord('home-planet', { name: 'Umber', id: '123' }); + const json = {}; this.owner.register( 'serializer:home-planet', @@ -602,7 +600,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }) ); - let serializer = store.serializerFor('home-planet'); + const serializer = store.serializerFor('home-planet'); serializer.serializeIntoHash(json, store.modelFor('home-planet'), league._createSnapshot()); @@ -614,8 +612,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test('normalizeResponse with async polymorphic belongsTo, using Type', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = (store, type) => { if (type.modelName === 'doomsday-device') { @@ -641,19 +639,14 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }; }; - await store - .findRecord('doomsday-device', 1) - .then((deathRay) => { - return deathRay.evilMinion; - }) - .then((evilMinion) => { - assert.strictEqual(evilMinion.eyes, 3); - }); + const deathRay = await store.findRecord('doomsday-device', '1'); + const evilMinion = await deathRay.evilMinion; + assert.strictEqual(evilMinion.eyes, 3); }); test('normalizeResponse with async polymorphic belongsTo', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = () => { return { @@ -709,8 +702,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { this.owner.register('model:evil-minion', EvilMinion); this.owner.register('model:yellow-minion', YellowMinion); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = () => { return { @@ -752,20 +745,19 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test('normalizeResponse can load secondary records of the same type without affecting the query count', function (assert) { - let jsonHash = { + const jsonHash = { comments: [{ id: '1', body: 'Parent Comment', root: true, children: [2, 3] }], _comments: [ { id: '2', body: 'Child Comment 1', root: false }, { id: '3', body: 'Child Comment 2', root: false }, ], }; - let array; this.owner.register('serializer:comment', JSONSerializer); - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); - array = serializer.normalizeResponse(store, Comment, jsonHash, '1', 'findRecord'); + const array = serializer.normalizeResponse(store, Comment, jsonHash, '1', 'findRecord'); assert.deepEqual(array, { data: { @@ -808,8 +800,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test("don't polymorphically deserialize base on the type key in payload when a type attribute exist", async function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); store.push( serializer.normalizeArrayResponse(store, store.modelFor('basket'), { @@ -832,8 +824,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test("don't polymorphically deserialize base on the type key in payload when a type attribute exist on a singular response", function (assert) { - let store = this.owner.lookup('service:store'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('application'); store.push( serializer.normalizeSingleResponse( @@ -853,8 +845,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }); test("don't polymorphically deserialize based on the type key in payload when a relationship exists named type", async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = () => { return { @@ -886,7 +878,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }) ); - let jsonHash = { + const jsonHash = { 'super-villains': [ { firstName: 'Tom', @@ -898,9 +890,9 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { ], }; - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let documentHash = store + const documentHash = store .serializerFor('super-villain') .normalizeSingleResponse(store, store.modelFor('super-villain'), jsonHash); @@ -912,7 +904,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { this.owner.register('serializer:evil-minion', JSONSerializer); this.owner.register('serializer:doomsday-device', RESTSerializer.extend()); - let payload = { + const payload = { doomsdayDevice: { id: '1', evilMinion: 2, @@ -923,8 +915,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); - let document = store + const store = this.owner.lookup('service:store'); + const document = store .serializerFor('doomsday-device') .normalizeSingleResponse(store, store.modelFor('doomsday-device'), payload); @@ -950,7 +942,7 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { this.owner.register('serializer:super-villain', JSONSerializer); this.owner.register('serializer:home-planet', RESTSerializer.extend()); - let payload = { + const payload = { homePlanet: { id: '1', superVillains: [2], @@ -961,8 +953,8 @@ module('integration/serializer/rest - RESTSerializer', function (hooks) { }, }; - let store = this.owner.lookup('service:store'); - let document = store + const store = this.owner.lookup('service:store'); + const document = store .serializerFor('home-planet') .normalizeSingleResponse(store, store.modelFor('home-planet'), payload); diff --git a/tests/main/tests/integration/snapshot-test.js b/tests/main/tests/integration/snapshot-test.js index c80c130bf77..8accd4572ef 100644 --- a/tests/main/tests/integration/snapshot-test.js +++ b/tests/main/tests/integration/snapshot-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -9,7 +8,11 @@ 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, _Post; +let owner, store; + +function isSnapshot(snapshot) { + return snapshot instanceof Snapshot || snapshot.constructor.name === 'Snapshot'; +} module('integration/snapshot - Snapshot', function (hooks) { setupTest(hooks); @@ -32,7 +35,6 @@ module('integration/snapshot - Snapshot', function (hooks) { @belongsTo('post', { async: true, inverse: 'comments' }) post; } - _Post = Post; owner = this.owner; owner.register('model:post', Post); @@ -56,15 +58,15 @@ module('integration/snapshot - Snapshot', function (hooks) { } owner.register('model:address', Address); - let newAddress = store.createRecord('address', {}); - let snapshot = newAddress._createSnapshot(); - let expected = { + const newAddress = store.createRecord('address', {}); + const snapshot = newAddress._createSnapshot(); + const expected = { country: 'USA', state: 'CA', street: undefined, }; - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); assert.deepEqual(snapshot.attributes(), expected, 'We generated attributes with default values'); store.destroy(); @@ -82,10 +84,10 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); - assert.ok(snapshot instanceof Snapshot, 'snapshot is an instance of Snapshot'); + assert.ok(isSnapshot(snapshot), 'snapshot is an instance of Snapshot'); }); test('snapshot.id, and snapshot.modelName returns correctly', function (assert) { @@ -100,8 +102,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); assert.strictEqual(snapshot.id, '1', 'id is correct'); assert.strictEqual(snapshot.modelName, 'post', 'modelName is correct'); @@ -118,7 +120,7 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.expect(3); let postClassLoaded = false; - let modelFor = store.modelFor; + const modelFor = store.modelFor; store.modelFor = (name) => { if (name === 'post') { postClassLoaded = true; @@ -135,12 +137,14 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); - let snapshot = await store._fetchManager.createSnapshot(identifier); + 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'); - assert.strictEqual(snapshot.type, _Post, 'type is correct'); + const type = snapshot.type; assert.true(postClassLoaded, 'model class is loaded'); + const Post = store.modelFor('post'); + assert.strictEqual(type, Post, 'type is correct'); } ); @@ -151,7 +155,7 @@ module('integration/snapshot - Snapshot', function (hooks) { const record = store._instanceCache.peek({ identifier, bucket: 'record' }); assert.false(!!record, 'We do not have a materialized record'); assert.strictEqual(snapshot.__attributes, null, 'attributes were not populated initially'); - return resolve({ + return Promise.resolve({ data: { type: 'post', id: '1', @@ -178,9 +182,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); - let snapshot = store._fetchManager.createSnapshot(identifier); - let expected = { + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); + const snapshot = store._fetchManager.createSnapshot(identifier); + const expected = { author: undefined, title: 'Hello World', }; @@ -203,9 +207,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); - let snapshot = store._fetchManager.createSnapshot(identifier); - let expected = { + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); + const snapshot = store._fetchManager.createSnapshot(identifier); + const expected = { author: undefined, title: 'Hello World', }; @@ -225,8 +229,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); assert.strictEqual(snapshot.attr('title'), 'Hello World', 'snapshot title is correct'); post.set('title', 'Tomster'); @@ -245,8 +249,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); assert.expectAssertion( () => { snapshot.attr('unknown'); @@ -268,10 +272,10 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); - let attributes = snapshot.attributes(); + const attributes = snapshot.attributes(); assert.deepEqual(attributes, { author: undefined, title: 'Hello World' }, 'attributes are returned correctly'); }); @@ -288,11 +292,11 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); post.set('title', 'Hello World!'); - let snapshot = post._createSnapshot(); + const snapshot = post._createSnapshot(); - let changes = snapshot.changedAttributes(); + const changes = snapshot.changedAttributes(); assert.deepEqual(changes.title, ['Hello World', 'Hello World!'], 'changed attributes are returned correctly'); }); @@ -309,9 +313,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let comment = store.peekRecord('comment', 1); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const comment = store.peekRecord('comment', 1); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); assert.strictEqual(relationship, undefined, 'relationship is undefined'); }); @@ -342,9 +346,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment = store.peekRecord('comment', 2); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const comment = store.peekRecord('comment', 2); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); assert.strictEqual(relationship, null, 'relationship is unset'); }); @@ -375,11 +379,11 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment = store.peekRecord('comment', 2); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const comment = store.peekRecord('comment', 2); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); - assert.ok(relationship instanceof Snapshot, 'snapshot is an instance of Snapshot'); + assert.ok(isSnapshot(relationship), 'snapshot is an instance of Snapshot'); assert.strictEqual(relationship.id, '1', 'post id is correct'); assert.strictEqual(relationship.attr('title'), 'Hello World', 'post title is correct'); }); @@ -412,11 +416,11 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment = store.peekRecord('comment', 2); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const comment = store.peekRecord('comment', 2); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); - assert.ok(relationship instanceof Snapshot, 'snapshot is an instance of Snapshot'); + assert.ok(isSnapshot(relationship), 'snapshot is an instance of Snapshot'); assert.deepEqual(relationship.changedAttributes(), {}, 'changedAttributes are correct'); }); @@ -446,13 +450,13 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', 1); - let comment = store.peekRecord('comment', 2); + const post = store.peekRecord('post', 1); + const comment = store.peekRecord('comment', 2); post.deleteRecord(); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); assert.strictEqual(relationship, null, 'relationship unset after deleted'); }); @@ -476,9 +480,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let comment = store.peekRecord('comment', 2); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post'); + const comment = store.peekRecord('comment', 2); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post'); assert.strictEqual(relationship, undefined, 'relationship is undefined'); }); @@ -487,7 +491,7 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.expect(2); store.adapterFor('application').findBelongsTo = function (store, snapshot, link, relationship) { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }; store.push({ @@ -506,7 +510,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let comment = store.peekRecord('comment', 2); + const comment = store.peekRecord('comment', 2); assert.strictEqual(comment._createSnapshot().belongsTo('post'), undefined, 'relationship is undefined'); await comment.post; @@ -525,8 +529,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); assert.expectAssertion( () => { @@ -539,7 +543,7 @@ module('integration/snapshot - Snapshot', function (hooks) { test('snapshot.belongsTo() returns a snapshot if relationship link has been fetched', async function (assert) { store.adapterFor('application').findBelongsTo = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'post', @@ -588,16 +592,16 @@ module('integration/snapshot - Snapshot', function (hooks) { // fetch the link const post = await comment.post; - let postSnapshot = post._createSnapshot(); - let commentSnapshot = comment._createSnapshot(); + const postSnapshot = post._createSnapshot(); + const commentSnapshot = comment._createSnapshot(); - let hasManyRelationship = postSnapshot.hasMany('comments'); - let belongsToRelationship = commentSnapshot.belongsTo('post'); + const hasManyRelationship = postSnapshot.hasMany('comments'); + const belongsToRelationship = commentSnapshot.belongsTo('post'); assert.ok(hasManyRelationship instanceof Array, 'hasMany relationship is an instance of Array'); assert.strictEqual(hasManyRelationship.length, 1, 'hasMany relationship contains related object'); - assert.ok(belongsToRelationship instanceof Snapshot, 'belongsTo relationship is an instance of Snapshot'); + assert.ok(isSnapshot(belongsToRelationship), 'belongsTo relationship is an instance of Snapshot'); assert.strictEqual( belongsToRelationship.attr('title'), 'Hello World', @@ -626,22 +630,22 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', '1'); - let comment = store.peekRecord('comment', '2'); + const post = store.peekRecord('post', '1'); + const comment = store.peekRecord('comment', '2'); const comments = await post.comments; comments.push(comment); - let postSnapshot = post._createSnapshot(); - let commentSnapshot = comment._createSnapshot(); + const postSnapshot = post._createSnapshot(); + const commentSnapshot = comment._createSnapshot(); - let hasManyRelationship = postSnapshot.hasMany('comments'); - let belongsToRelationship = commentSnapshot.belongsTo('post'); + const hasManyRelationship = postSnapshot.hasMany('comments'); + const belongsToRelationship = commentSnapshot.belongsTo('post'); assert.ok(hasManyRelationship instanceof Array, 'hasMany relationship is an instance of Array'); assert.strictEqual(hasManyRelationship.length, 1, 'hasMany relationship contains related object'); - assert.ok(belongsToRelationship instanceof Snapshot, 'belongsTo relationship is an instance of Snapshot'); + assert.ok(isSnapshot(belongsToRelationship), 'belongsTo relationship is an instance of Snapshot'); assert.strictEqual( belongsToRelationship.attr('title'), 'Hello World', @@ -670,21 +674,21 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', 1); - let comment = store.peekRecord('comment', 2); + const post = store.peekRecord('post', 1); + const comment = store.peekRecord('comment', 2); comment.set('post', post); - let postSnapshot = post._createSnapshot(); - let commentSnapshot = comment._createSnapshot(); + const postSnapshot = post._createSnapshot(); + const commentSnapshot = comment._createSnapshot(); - let hasManyRelationship = postSnapshot.hasMany('comments'); - let belongsToRelationship = commentSnapshot.belongsTo('post'); + const hasManyRelationship = postSnapshot.hasMany('comments'); + const belongsToRelationship = commentSnapshot.belongsTo('post'); assert.ok(hasManyRelationship instanceof Array, 'hasMany relationship is an instance of Array'); assert.strictEqual(hasManyRelationship.length, 1, 'hasMany relationship contains related object'); - assert.ok(belongsToRelationship instanceof Snapshot, 'belongsTo relationship is an instance of Snapshot'); + assert.ok(isSnapshot(belongsToRelationship), 'belongsTo relationship is an instance of Snapshot'); assert.strictEqual( belongsToRelationship.attr('title'), 'Hello World', @@ -718,9 +722,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment = store.peekRecord('comment', 2); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post', { id: true }); + const comment = store.peekRecord('comment', 2); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post', { id: true }); assert.strictEqual(relationship, '1', 'relationship ID correctly returned'); }); @@ -751,13 +755,13 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', 1); - let comment = store.peekRecord('comment', 2); + const post = store.peekRecord('post', 1); + const comment = store.peekRecord('comment', 2); post.deleteRecord(); - let snapshot = comment._createSnapshot(); - let relationship = snapshot.belongsTo('post', { id: true }); + const snapshot = comment._createSnapshot(); + const relationship = snapshot.belongsTo('post', { id: true }); assert.strictEqual(relationship, null, 'relationship unset after deleted'); }); @@ -774,9 +778,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.strictEqual(relationship, undefined, 'relationship is undefined'); }); @@ -798,9 +802,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.ok(relationship instanceof Array, 'relationship is an instance of Array'); assert.strictEqual(relationship.length, 0, 'relationship is empty'); @@ -842,16 +846,16 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let post = store.peekRecord('post', 3); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const post = store.peekRecord('post', 3); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.ok(relationship instanceof Array, 'relationship is an instance of Array'); assert.strictEqual(relationship.length, 2, 'relationship has two items'); - let relationship1 = relationship[0]; + const relationship1 = relationship[0]; - assert.ok(relationship1 instanceof Snapshot, 'relationship item is an instance of Snapshot'); + assert.ok(isSnapshot(relationship1), 'relationship item is an instance of Snapshot'); assert.strictEqual(relationship1.id, '1', 'relationship item id is correct'); assert.strictEqual(relationship1.attr('body'), 'This is the first comment', 'relationship item body is correct'); }); @@ -892,15 +896,15 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let post = store.peekRecord('post', 3); + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const post = store.peekRecord('post', 3); comment1.deleteRecord(); comment2.deleteRecord(); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.ok(relationship instanceof Array, 'relationship is an instance of Array'); assert.strictEqual(relationship.length, 0, 'relationship is empty'); @@ -926,9 +930,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments', { ids: true }); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments', { ids: true }); assert.deepEqual(relationship, ['2', '3'], 'relationship IDs correctly returned'); }); @@ -969,15 +973,15 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment1 = store.peekRecord('comment', 1); - let comment2 = store.peekRecord('comment', 2); - let post = store.peekRecord('post', 3); + const comment1 = store.peekRecord('comment', 1); + const comment2 = store.peekRecord('comment', 2); + const post = store.peekRecord('post', 3); comment1.deleteRecord(); comment2.deleteRecord(); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments', { ids: true }); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments', { ids: true }); assert.ok(relationship instanceof Array, 'relationship is an instance of Array'); assert.strictEqual(relationship.length, 0, 'relationship is empty'); @@ -1002,9 +1006,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.strictEqual(relationship, undefined, 'relationship is undefined'); }); @@ -1013,7 +1017,7 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.expect(2); store.adapterFor('application').findHasMany = function (store, snapshot, link, relationship) { - return resolve({ + return Promise.resolve({ data: [{ id: '2', type: 'comment', attributes: { body: 'This is comment' } }], }); }; @@ -1035,11 +1039,11 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); await post.comments.then((comments) => { - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + const snapshot = post._createSnapshot(); + const relationship = snapshot.hasMany('comments'); assert.ok(relationship instanceof Array, 'relationship is an instance of Array'); assert.strictEqual(relationship.length, 1, 'relationship has one item'); @@ -1058,8 +1062,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); assert.expectAssertion( () => { @@ -1071,9 +1075,9 @@ module('integration/snapshot - Snapshot', function (hooks) { }); test('snapshot.hasMany() respects the order of items in the relationship', async function (assert) { - assert.expect(3); + assert.expect(10); - store.push({ + const [comment1, comment2, comment3, post] = store.push({ data: [ { type: 'comment', @@ -1114,18 +1118,86 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment3 = store.peekRecord('comment', 3); - let post = store.peekRecord('post', 4); const comments = await post.comments; + let snapshot = post._createSnapshot(); + let relationship = snapshot.hasMany('comments'); + + assert.arrayStrictEquals(comments, [comment1, comment2, comment3], 'initial relationship order is correct'); + assert.arrayStrictEquals( + relationship.map((s) => s.id), + ['1', '2', '3'], + 'initial relationship reference order is correct' + ); + + // change order locally comments.splice(comments.indexOf(comment3), 1); comments.unshift(comment3); - let snapshot = post._createSnapshot(); - let relationship = snapshot.hasMany('comments'); + snapshot = post._createSnapshot(); + relationship = snapshot.hasMany('comments'); + + assert.arrayStrictEquals(comments, [comment3, comment1, comment2], 'relationship preserved local order'); + assert.arrayStrictEquals( + relationship.map((s) => s.id), + ['3', '1', '2'], + 'relationship reference preserved local order' + ); - assert.strictEqual(relationship[0].id, '3', 'order of comment 3 is correct'); - assert.strictEqual(relationship[1].id, '1', 'order of comment 1 is correct'); - assert.strictEqual(relationship[2].id, '2', 'order of comment 2 is correct'); + // change order locally again + comments.splice(comments.indexOf(comment1), 1); + + snapshot = post._createSnapshot(); + relationship = snapshot.hasMany('comments'); + + assert.arrayStrictEquals(comments, [comment3, comment2], 'relationship preserved local order'); + assert.arrayStrictEquals( + relationship.map((s) => s.id), + ['3', '2'], + 'relationship reference preserved local order' + ); + + // and again + comments.push(comment1); + + snapshot = post._createSnapshot(); + relationship = snapshot.hasMany('comments'); + + assert.arrayStrictEquals(comments, [comment3, comment2, comment1], 'relationship preserved local order'); + assert.arrayStrictEquals( + relationship.map((s) => s.id), + ['3', '2', '1'], + 'relationship reference preserved local order' + ); + + // push a new remote state with a different order + store.push({ + data: { + type: 'post', + id: '4', + attributes: { + title: 'Hello World', + }, + relationships: { + comments: { + data: [ + { type: 'comment', id: '3' }, + { type: 'comment', id: '1' }, + { type: 'comment', id: '2' }, + ], + }, + }, + }, + }); + + snapshot = post._createSnapshot(); + relationship = snapshot.hasMany('comments'); + + assert.arrayStrictEquals(comments, [comment3, comment1, comment2], 'relationship updated to remote order'); + assert.arrayStrictEquals( + relationship.map((s) => s.id), + ['3', '1', '2'], + 'relationship updated to remote order' + ); }); test('snapshot.eachAttribute() proxies to record', function (assert) { @@ -1140,10 +1212,10 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); - let attributes = []; + const attributes = []; snapshot.eachAttribute((name) => attributes.push(name)); assert.deepEqual(attributes, ['author', 'title'], 'attributes are iterated correctly'); }); @@ -1151,8 +1223,8 @@ module('integration/snapshot - Snapshot', function (hooks) { test('snapshot.eachRelationship() proxies to record', function (assert) { assert.expect(2); - let getRelationships = function (snapshot) { - let relationships = []; + const getRelationships = function (snapshot) { + const relationships = []; snapshot.eachRelationship((name) => relationships.push(name)); return relationships; }; @@ -1175,8 +1247,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, ], }); - let comment = store.peekRecord('comment', 1); - let post = store.peekRecord('post', 2); + const comment = store.peekRecord('comment', 1); + const post = store.peekRecord('post', 2); let snapshot; snapshot = comment._createSnapshot(); @@ -1207,8 +1279,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let comment = store.peekRecord('comment', 1); - let snapshot = comment._createSnapshot(); + const comment = store.peekRecord('comment', 1); + const snapshot = comment._createSnapshot(); snapshot.belongsTo('post'); }); @@ -1237,8 +1309,8 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); snapshot.hasMany('comments'); }); @@ -1255,12 +1327,12 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let post = store.peekRecord('post', 1); - let snapshot = post._createSnapshot(); + const post = store.peekRecord('post', 1); + const snapshot = post._createSnapshot(); post.set('title', 'New Title'); - let expected = { + const expected = { data: { attributes: { author: undefined, diff --git a/tests/main/tests/integration/store-extension-test.ts b/tests/main/tests/integration/store-extension-test.ts new file mode 100644 index 00000000000..0e19d59bd8f --- /dev/null +++ b/tests/main/tests/integration/store-extension-test.ts @@ -0,0 +1,48 @@ +import { inject as service } from '@ember/service'; + +import { module, test } from 'qunit'; + +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import RequestManager from '@ember-data/request'; + +module('Integration | Store Extension', function (hooks) { + setupTest(hooks); + + test('We can create a store ', function (assert) { + const { owner } = this; + class CustomStore extends Store {} + owner.register('service:store', CustomStore); + const store = owner.lookup('service:store') as CustomStore; + + assert.true(typeof store.requestManager !== 'undefined', 'We create a request manager for the store automatically'); + }); + + test('We can create a store with a custom request manager injected as a service', function (assert) { + const { owner } = this; + class CustomStore extends Store { + @service declare requestManager: RequestManager; + } + + owner.register('service:store', CustomStore); + owner.register('service:request-manager', RequestManager); + const requestManager = owner.lookup('service:request-manager'); + const store = owner.lookup('service:store') as CustomStore; + + assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store'); + }); + + test('We can create a store with a custom request manager initialized as a field', function (assert) { + const { owner } = this; + const requestManager = new RequestManager(); + class CustomStore extends Store { + requestManager = requestManager; + } + + owner.register('service:store', CustomStore); + const store = owner.lookup('service:store') as CustomStore; + + assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store'); + }); +}); diff --git a/tests/main/tests/integration/store-test.js b/tests/main/tests/integration/store-test.js index 6a540171e45..5d3b619e371 100644 --- a/tests/main/tests/integration/store-test.js +++ b/tests/main/tests/integration/store-test.js @@ -1,8 +1,6 @@ -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -12,7 +10,6 @@ import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Person extends Model { @@ -36,17 +33,17 @@ class Car extends Model { function ajaxResponse(value) { return function (url, verb, hash) { - return resolve(deepCopy(value)); + return Promise.resolve(structuredClone(value)); }; } function tap(obj, methodName, callback) { - let old = obj[methodName]; + const old = obj[methodName]; - let summary = { called: [] }; + const summary = { called: [] }; obj[methodName] = function () { - let result = old.apply(obj, arguments); + const result = old.apply(obj, arguments); if (callback) { callback.apply(obj, arguments); } @@ -71,9 +68,9 @@ module('integration/store - destroy', function (hooks) { test("destroying record during find doesn't cause unexpected error (find resolves)", async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let TestAdapter = Adapter.extend({ + const TestAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { return new Promise((resolve, reject) => { store.unloadAll(type.modelName); @@ -90,8 +87,8 @@ module('integration/store - destroy', function (hooks) { this.owner.register('adapter:application', TestAdapter); - let type = 'car'; - let id = '1'; + const type = 'car'; + const id = '1'; try { await store.findRecord(type, id); @@ -104,9 +101,9 @@ module('integration/store - destroy', function (hooks) { test("destroying record during find doesn't cause unexpected error (find rejects)", async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let TestAdapter = Adapter.extend({ + const TestAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { return new Promise((resolve, reject) => { store.unloadAll(type.modelName); @@ -117,8 +114,8 @@ module('integration/store - destroy', function (hooks) { this.owner.register('adapter:application', TestAdapter); - let type = 'car'; - let id = '1'; + const type = 'car'; + const id = '1'; try { await store.findRecord(type, id); @@ -161,7 +158,10 @@ module('integration/store - destroy', function (hooks) { // ensure we make it into the adapter await arrivedPromise; - run(() => store.destroy()); + store.destroy(); + // can't use await settled() since pending promise + // is in the waiter system + await new Promise((r) => setTimeout(r, 0)); // release the adapter promise next(); @@ -172,14 +172,13 @@ module('integration/store - destroy', function (hooks) { await requestPromise; assert.ok(false, 'We should reject with a meaningful error'); } catch (e) { - assert.strictEqual(e.message, 'Assertion Failed: Async Leak Detected: Expected the store to not be destroyed'); + assert.strictEqual(e.message, 'Async Leak Detected: Expected the store to not be destroyed'); } }); test('destroying the store correctly cleans everything up', async function (assert) { - let car, person; - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -213,11 +212,11 @@ module('integration/store - destroy', function (hooks) { ], }); - car = store.peekRecord('car', '1'); - person = store.peekRecord('person', '1'); + const car = store.peekRecord('car', '1'); + const person = store.peekRecord('person', '1'); - let personWillDestroy = tap(person, 'willDestroy'); - let carWillDestroy = tap(car, 'willDestroy'); + const personWillDestroy = tap(person, 'willDestroy'); + const carWillDestroy = tap(car, 'willDestroy'); const cars = car.person.cars; adapter.query = function () { @@ -232,7 +231,7 @@ module('integration/store - destroy', function (hooks) { }; }; - let adapterPopulatedPeople = await store.query('person', { + const adapterPopulatedPeople = await store.query('person', { someCrazy: 'query', }); @@ -274,7 +273,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord fetches record from server when cached record is not present', async function (assert) { assert.expect(2); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); adapter.ajax = ajaxResponse({ cars: [ @@ -286,11 +285,11 @@ module('integration/store - findRecord', function (hooks) { ], }); - let cachedRecordIsPresent = store.peekRecord('car', '20') !== null; + const cachedRecordIsPresent = store.peekRecord('car', '20') !== null; assert.notOk(cachedRecordIsPresent, 'Car with id=20 should not exist'); - let car = await store.findRecord('car', '20'); + const car = await store.findRecord('car', '20'); assert.strictEqual(car.make, 'BMC', 'Car with id=20 is now loaded'); }); @@ -298,7 +297,7 @@ module('integration/store - findRecord', function (hooks) { test('store#findRecord returns cached record immediately and reloads record in the background', async function (assert) { assert.expect(2); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); adapter.shouldReloadRecord = () => false; adapter.shouldBackgroundReloadRecord = () => true; @@ -315,7 +314,7 @@ module('integration/store - findRecord', function (hooks) { }); let resolver; - let promise = new Promise((r) => (resolver = r)); + const promise = new Promise((r) => (resolver = r)); adapter.ajax = async () => { await promise; @@ -344,15 +343,15 @@ module('integration/store - findRecord', function (hooks) { assert.expect(6); this.owner.unregister('serializer:application'); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); let calls = 0; delete adapter.shouldReloadRecord; adapter.shouldBackgroundReloadRecord = () => true; adapter.findRecord = () => { - if (calls++ < 3) { - return resolve({ + if (calls++ < 4) { + return Promise.resolve({ data: { type: 'car', id: '1', @@ -371,6 +370,7 @@ module('integration/store - findRecord', function (hooks) { const car = await proxiedCar; // load 1 assert.strictEqual(car.model, 'Mini', 'car record is returned from cache'); + const proxiedCar2 = store.findRecord('car', '1'); // will trigger a backgroundReload const car2 = await proxiedCar2; @@ -387,14 +387,14 @@ module('integration/store - findRecord', function (hooks) { await store._getAllPending(); - assert.strictEqual(calls, 3, 'we triggered one background reload and one load'); + assert.strictEqual(calls, 3, 'we triggered two background reloads and one load'); }); test('multiple parallel calls to store#findRecord return the cached record without waiting for background requests', async function (assert) { assert.expect(8); this.owner.unregister('serializer:application'); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); let calls = 0; delete adapter.shouldReloadRecord; @@ -407,7 +407,7 @@ module('integration/store - findRecord', function (hooks) { adapter.findRecord = async () => { await timeout(1); if (calls++ < 2) { - return resolve({ + return Promise.resolve({ data: { type: 'car', id: '1', @@ -459,7 +459,7 @@ module('integration/store - findRecord', function (hooks) { this.owner.register('adapter:application', testAdapter); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -482,11 +482,11 @@ module('integration/store - findRecord', function (hooks) { ], }); - let cachedCar = store.peekRecord('car', '1'); + const cachedCar = store.peekRecord('car', '1'); assert.strictEqual(cachedCar.model, 'Mini', 'cached car has expected model'); - let car = await store.findRecord('car', '1', { reload: true }); + const car = await store.findRecord('car', '1', { reload: true }); assert.strictEqual(car.model, 'Princess', 'cached record ignored, record reloaded via server'); }); @@ -503,7 +503,7 @@ module('integration/store - findRecord', function (hooks) { async findRecord() { calls++; - await resolve(); + await Promise.resolve(); return { data: { @@ -537,10 +537,10 @@ module('integration/store - findRecord', function (hooks) { let calls = 0; let resolveHandler; - let deferred = new Promise((resolve) => { + const deferred = new Promise((resolve) => { resolveHandler = resolve; }); - let result = { + const result = { data: { type: 'car', id: '1', @@ -564,14 +564,13 @@ module('integration/store - findRecord', function (hooks) { this.owner.register('adapter:application', TestAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let firstPromise, secondPromise; - firstPromise = store.findRecord('car', '1', { reload: true }); - secondPromise = store.findRecord('car', '1', { reload: true }); + const firstPromise = store.findRecord('car', '1', { reload: true }); + const secondPromise = store.findRecord('car', '1', { reload: true }); resolveHandler(result); - let car1 = await firstPromise; - let car2 = await secondPromise; + const car1 = await firstPromise; + const car2 = await secondPromise; assert.strictEqual(calls, 1, 'We made one call to findRecord'); assert.strictEqual(car1, car2, 'we receive the same car back'); @@ -660,7 +659,7 @@ module('integration/store - findRecord', function (hooks) { this.owner.register('adapter:application', testAdapter); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -674,11 +673,11 @@ module('integration/store - findRecord', function (hooks) { }); let resolver; - let promise = new Promise((r) => (resolver = r)); + const promise = new Promise((r) => (resolver = r)); adapter.ajax = async function () { await promise; - return deepCopy({ + return structuredClone({ cars: [ { id: '1', @@ -689,7 +688,7 @@ module('integration/store - findRecord', function (hooks) { }); }; - let carPromise = await store.findRecord('car', '1', { backgroundReload: true }); + const carPromise = await store.findRecord('car', '1', { backgroundReload: true }); assert.strictEqual(carPromise.model, 'Mini', 'cached car record is returned'); @@ -697,7 +696,7 @@ module('integration/store - findRecord', function (hooks) { resolver(); await settled(); - let car = store.peekRecord('car', '1'); + const car = store.peekRecord('car', '1'); assert.strictEqual(car.model, 'Princess', 'car record was reloaded'); }); @@ -717,7 +716,7 @@ module('integration/store - findRecord', function (hooks) { this.owner.register('adapter:application', testAdapter); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -731,9 +730,9 @@ module('integration/store - findRecord', function (hooks) { }); adapter.ajax = async function () { - await resolve(); + await Promise.resolve(); - return deepCopy({ + return structuredClone({ cars: [ { id: '1', @@ -759,9 +758,12 @@ module('integration/store - findRecord', function (hooks) { assert.expect(badValues.length); badValues.map((item) => { - assert.expectAssertion(() => { - store.findRecord('car', item); - }, `Expected id to be a string or number, received ${String(item)}`); + assert.expectAssertion( + () => { + store.findRecord('car', item); + }, + `Expected id to be a string or number, received ${String(item)}` + ); }); }); }); @@ -776,11 +778,11 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.ajax = () => { - return resolve({ + return Promise.resolve({ cars: [ { id: '1', @@ -812,8 +814,8 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -869,7 +871,7 @@ module('integration/store - findAll', function (hooks) { assert.strictEqual(cars.length, 2, 'There is 2 cars in the store now'); - let mini = cars.find((car) => car.id === '1'); + const mini = cars.find((car) => car.id === '1'); assert.strictEqual(mini.model, 'New Mini', 'Existing records have been updated'); }); @@ -877,7 +879,7 @@ module('integration/store - findAll', function (hooks) { test('store#findAll { backgroundReload: false } skips shouldBackgroundReloadAll, returns cached records & does not reload in the background', async function (assert) { assert.expect(4); - let testAdapter = RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldBackgroundReloadAll() { assert.ok(false, 'shouldBackgroundReloadAll should not be called when { backgroundReload: false }'); }, @@ -891,7 +893,7 @@ module('integration/store - findAll', function (hooks) { this.owner.register('serializer:application', RESTSerializer.extend()); this.owner.register('adapter:application', testAdapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -920,7 +922,7 @@ module('integration/store - findAll', function (hooks) { test('store#findAll { backgroundReload: true } skips shouldBackgroundReloadAll, returns cached records, & reloads in background', async function (assert) { assert.expect(5); - let testAdapter = RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldBackgroundReloadAll() { assert.ok(false, 'shouldBackgroundReloadAll should not be called when { backgroundReload: true }'); }, @@ -930,8 +932,8 @@ module('integration/store - findAll', function (hooks) { this.owner.register('model:car', Car); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -945,7 +947,7 @@ module('integration/store - findAll', function (hooks) { }); let resolve; - let promise = new Promise((r) => { + const promise = new Promise((r) => { resolve = r; }); adapter.ajax = async () => { @@ -985,7 +987,7 @@ module('integration/store - findAll', function (hooks) { test('store#findAll { backgroundReload: false } is ignored if adapter.shouldReloadAll is true', async function (assert) { assert.expect(5); - let testAdapter = RESTAdapter.extend({ + const testAdapter = RESTAdapter.extend({ shouldReloadAll() { return true; }, @@ -999,8 +1001,8 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', testAdapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.push({ data: { @@ -1014,7 +1016,7 @@ module('integration/store - findAll', function (hooks) { }); adapter.ajax = () => { - return resolve({ + return Promise.resolve({ cars: [ { id: '1', @@ -1049,8 +1051,8 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); store.push({ data: [ @@ -1095,7 +1097,7 @@ module('integration/store - findAll', function (hooks) { return resolvefindAllPromise; }; - let cars = await store.findAll('car'); + const cars = await store.findAll('car'); assert.strictEqual(cars.length, 2, 'It returns all cars'); @@ -1122,11 +1124,11 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.ajax = () => { - return resolve({ + return Promise.resolve({ cars: [ { id: '20', @@ -1154,7 +1156,7 @@ module('integration/store - findAll', function (hooks) { assert.strictEqual(store.peekRecord('car', '20'), null, 'the car is not loaded'); - let car = await store.findRecord('car', '20'); + const car = await store.findRecord('car', '20'); assert.strictEqual(car.make, 'BMCW', 'Car with id=20 is now loaded'); }); @@ -1166,13 +1168,13 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); try { - let applicationAdapter = store.adapterFor('application'); + const applicationAdapter = store.adapterFor('application'); assert.ok(applicationAdapter); - } catch (_error) { + } catch { assert.ok(false, 'An error was thrown while looking for application adapter'); } }); @@ -1184,13 +1186,13 @@ module('integration/store - findAll', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); try { - let applicationSerializer = store.serializerFor('application'); + const applicationSerializer = store.serializerFor('application'); assert.ok(applicationSerializer); - } catch (_error) { + } catch { assert.ok(false, 'An error was thrown while looking for application serializer'); } }); @@ -1206,8 +1208,7 @@ module('integration/store - deleteRecord', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let person; + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -1219,7 +1220,7 @@ module('integration/store - deleteRecord', function (hooks) { }, }); - person = store.peekRecord('person', '1'); + const person = store.peekRecord('person', '1'); assert.notStrictEqual(store.peekRecord('person', '1'), null, 'expected the record to be in the store'); @@ -1233,11 +1234,11 @@ module('integration/store - deleteRecord', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); try { store.push({ data: null }); - } catch (_error) { + } catch { assert.ok(false, 'push null value for `data` to store throws an error'); } }); @@ -1253,7 +1254,7 @@ module('integration/store - deleteRecord', function (hooks) { this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('model:car', Car); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async () => { await store.findRecord('car', '1'); @@ -1265,14 +1266,14 @@ module('integration/store - deleteRecord', function (hooks) { this.owner.register('adapter:application', RESTAdapter.extend()); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function () { return {}; }; - let car = store.createRecord('car'); + const car = store.createRecord('car'); await assert.expectAssertion(async () => { await car.save(); @@ -1292,9 +1293,9 @@ module('integration/store - queryRecord', function (hooks) { testInDebug( 'store#queryRecord should assert when normalized payload of adapter has an array of data', async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); - let serializer = store.serializerFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const serializer = store.serializerFor('application'); adapter.queryRecord = function () { return { @@ -1317,8 +1318,8 @@ module('integration/store - queryRecord', function (hooks) { test('The store should trap exceptions that are thrown from adapter#findRecord', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function () { throw new Error('Refusing to find record'); @@ -1334,8 +1335,8 @@ module('integration/store - queryRecord', function (hooks) { test('The store should trap exceptions that are thrown from adapter#findAll', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findAll = function () { throw new Error('Refusing to find all records'); @@ -1351,8 +1352,8 @@ module('integration/store - queryRecord', function (hooks) { test('The store should trap exceptions that are thrown from adapter#query', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.query = function () { throw new Error('Refusing to query records'); @@ -1368,8 +1369,8 @@ module('integration/store - queryRecord', function (hooks) { test('The store should trap exceptions that are thrown from adapter#queryRecord', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.queryRecord = function () { throw new Error('Refusing to query record'); @@ -1385,15 +1386,15 @@ module('integration/store - queryRecord', function (hooks) { test('The store should trap exceptions that are thrown from adapter#createRecord', async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.createRecord = function () { throw new Error('Refusing to serialize'); }; try { - let car = store.createRecord('car'); + const car = store.createRecord('car'); await car.save(); } catch (error) { diff --git a/tests/main/tests/integration/store/adapter-for-test.js b/tests/main/tests/integration/store/adapter-for-test.js index 65f97e167cc..785785670ff 100644 --- a/tests/main/tests/integration/store/adapter-for-test.js +++ b/tests/main/tests/integration/store/adapter-for-test.js @@ -1,10 +1,10 @@ -import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; -import Store from '@ember-data/store'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; class TestAdapter { @@ -25,13 +25,13 @@ module('integration/store - adapterFor', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; store = owner.lookup('service:store'); }); test('when no adapter is available we throw an error', async function (assert) { assert.expectAssertion(() => { - let { owner } = this; + const { owner } = this; /* adapter:-json-api is the "last chance" fallback and is the json-api adapter which is re-exported as app/adapters/-json-api. @@ -48,11 +48,11 @@ module('integration/store - adapterFor', function (hooks) { return lookup.call(owner, registrationName); }; store.adapterFor('person'); - }, /Assertion Failed: No adapter was found for 'person' and no 'application' adapter was found as a fallback/); + }, /No adapter was found for 'person' and no 'application' adapter was found as a fallback/); }); test('we find and instantiate the application adapter', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; class AppAdapter extends TestAdapter { @@ -63,13 +63,13 @@ module('integration/store - adapterFor', function (hooks) { owner.register('adapter:application', AppAdapter); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); assert.ok(adapter instanceof AppAdapter, 'We found the correct adapter'); assert.ok(didInstantiate, 'We instantiated the adapter'); didInstantiate = false; - let adapterAgain = store.adapterFor('application'); + const adapterAgain = store.adapterFor('application'); assert.ok(adapterAgain instanceof AppAdapter, 'We found the correct adapter'); assert.notOk(didInstantiate, 'We did not instantiate the adapter again'); @@ -77,7 +77,7 @@ module('integration/store - adapterFor', function (hooks) { }); test('multiple stores do not share adapters', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; class AppAdapter extends TestAdapter { @@ -89,14 +89,14 @@ module('integration/store - adapterFor', function (hooks) { owner.register('adapter:application', AppAdapter); owner.register('service:other-store', Store); - let otherStore = owner.lookup('service:other-store'); - let adapter = store.adapterFor('application'); + const otherStore = owner.lookup('service:other-store'); + const adapter = store.adapterFor('application'); assert.ok(adapter instanceof AppAdapter, 'We found the correct adapter'); assert.ok(didInstantiate, 'We instantiated the adapter'); didInstantiate = false; - let otherAdapter = otherStore.adapterFor('application'); + const otherAdapter = otherStore.adapterFor('application'); assert.ok(otherAdapter instanceof AppAdapter, 'We found the correct adapter again'); assert.ok(didInstantiate, 'We instantiated the other adapter'); assert.notStrictEqual(otherAdapter, adapter, 'We have a different adapter instance'); @@ -105,7 +105,7 @@ module('integration/store - adapterFor', function (hooks) { }); test('we can find and instantiate per-type adapters', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiateAppAdapter = false; let didInstantiatePersonAdapter = false; @@ -124,20 +124,20 @@ module('integration/store - adapterFor', function (hooks) { owner.register('adapter:application', AppAdapter); owner.register('adapter:person', PersonAdapter); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); assert.ok(adapter instanceof PersonAdapter, 'We found the correct adapter'); assert.ok(didInstantiatePersonAdapter, 'We instantiated the person adapter'); assert.notOk(didInstantiateAppAdapter, 'We did not instantiate the application adapter'); - let appAdapter = store.adapterFor('application'); + const appAdapter = store.adapterFor('application'); assert.ok(appAdapter instanceof AppAdapter, 'We found the correct adapter'); assert.ok(didInstantiateAppAdapter, 'We instantiated the application adapter'); assert.notStrictEqual(appAdapter, adapter, 'We have separate adapters'); }); test('we fallback to the application adapter when a per-type adapter is not found', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiateAppAdapter = false; class AppAdapter extends TestAdapter { @@ -148,13 +148,13 @@ module('integration/store - adapterFor', function (hooks) { owner.register('adapter:application', AppAdapter); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); assert.ok(adapter instanceof AppAdapter, 'We found the adapter'); assert.ok(didInstantiateAppAdapter, 'We instantiated the adapter'); didInstantiateAppAdapter = false; - let appAdapter = store.adapterFor('application'); + const appAdapter = store.adapterFor('application'); assert.ok(appAdapter instanceof AppAdapter, 'We found the correct adapter'); assert.notOk(didInstantiateAppAdapter, 'We did not instantiate the adapter again'); assert.strictEqual(appAdapter, adapter, 'We fell back to the application adapter instance'); @@ -168,7 +168,7 @@ module('integration/store - adapterFor', function (hooks) { count: 2, }, async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiateAdapter = false; @@ -189,19 +189,19 @@ module('integration/store - adapterFor', function (hooks) { owner.unregister('adapter:-json-api'); owner.register('adapter:-json-api', JsonApiAdapter); - let adapter = store.adapterFor('person'); + const adapter = store.adapterFor('person'); assert.ok(adapter instanceof JsonApiAdapter, 'We found the adapter'); assert.ok(didInstantiateAdapter, 'We instantiated the adapter'); didInstantiateAdapter = false; - let appAdapter = store.adapterFor('application'); + 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; - let jsonApiAdapter = store.adapterFor('-json-api'); + 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'); @@ -214,7 +214,7 @@ module('integration/store - adapterFor', function (hooks) { ); test('adapters are destroyed', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; let didDestroy = false; @@ -230,12 +230,13 @@ module('integration/store - adapterFor', function (hooks) { owner.register('adapter:application', AppAdapter); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); assert.ok(adapter instanceof AppAdapter, 'precond - We found the correct adapter'); assert.ok(didInstantiate, 'precond - We instantiated the adapter'); - run(store, 'destroy'); + store.destroy(); + await settled(); assert.ok(didDestroy, 'adapter was destroyed'); }); diff --git a/tests/main/tests/integration/store/json-api-validation-test.js b/tests/main/tests/integration/store/json-api-validation-test.js index 514b620a9b7..ac8041e5d78 100644 --- a/tests/main/tests/integration/store/json-api-validation-test.js +++ b/tests/main/tests/integration/store/json-api-validation-test.js @@ -1,5 +1,4 @@ -import QUnit, { module } from 'qunit'; -import { resolve } from 'rsvp'; +import { module } from 'qunit'; import { setupTest } from 'ember-qunit'; @@ -21,7 +20,7 @@ async function payloadError(owner, payload, expectedError, assert) { 'adapter:person', Adapter.extend({ findRecord() { - return resolve(payload); + return Promise.resolve(payload); }, }) ); @@ -39,8 +38,8 @@ async function payloadError(owner, payload, expectedError, assert) { module('integration/store/json-validation', function (hooks) { setupTest(hooks); - hooks.beforeEach(function () { - QUnit.assert.payloadError = payloadError.bind(QUnit.assert); + hooks.beforeEach(function (assert) { + assert.payloadError = payloadError; const Person = Model.extend({ updatedAt: attr('string'), @@ -52,10 +51,6 @@ module('integration/store/json-validation', function (hooks) { this.owner.register('model:person', Person); }); - hooks.afterEach(function () { - QUnit.assert.payloadError = null; - }); - testInDebug("when normalizeResponse returns undefined (or doesn't return), throws an error", async function (assert) { this.owner.register( 'serializer:person', @@ -68,12 +63,12 @@ module('integration/store/json-validation', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return resolve({ data: {} }); + return Promise.resolve({ data: {} }); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.findRecord('person', '1'); @@ -94,12 +89,12 @@ module('integration/store/json-validation', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return resolve({ data: {} }); + return Promise.resolve({ data: {} }); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.findRecord('person', '1'); @@ -120,12 +115,12 @@ module('integration/store/json-validation', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return resolve({ data: {} }); + return Promise.resolve({ data: {} }); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.findRecord('person', '1'); @@ -151,12 +146,12 @@ module('integration/store/json-validation', function (hooks) { 'adapter:person', Adapter.extend({ findRecord() { - return resolve({ data: {} }); + return Promise.resolve({ data: {} }); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.findRecord('person', '1'); diff --git a/tests/main/tests/integration/store/package-import-test.js b/tests/main/tests/integration/store/package-import-test.js index 47b932ac0c5..11820b7de7f 100644 --- a/tests/main/tests/integration/store/package-import-test.js +++ b/tests/main/tests/integration/store/package-import-test.js @@ -18,7 +18,7 @@ module('integration/store/package-import', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:person', Person); owner.unregister('service:store'); @@ -45,7 +45,7 @@ module('integration/store/package-import', function (hooks) { ], }); - let all = store.peekAll('person'); + const all = store.peekAll('person'); assert.strictEqual(get(all, 'length'), 2); store.push({ diff --git a/tests/main/tests/integration/store/query-record-test.js b/tests/main/tests/integration/store/query-record-test.js index 7a63f732499..a7159280f1e 100644 --- a/tests/main/tests/integration/store/query-record-test.js +++ b/tests/main/tests/integration/store/query-record-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -21,7 +20,7 @@ module('integration/store/query-record - Query one record with a query hash', fu }); testInDebug('It raises an assertion when no type is passed', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.queryRecord(); @@ -29,7 +28,7 @@ module('integration/store/query-record - Query one record with a query hash', fu }); testInDebug('It raises an assertion when no query hash is passed', async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectAssertion(async function () { await store.queryRecord('person'); @@ -39,15 +38,15 @@ module('integration/store/query-record - Query one record with a query hash', fu test("When a record is requested, the adapter's queryRecord method should be called.", async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let Person = store.modelFor('person'); + const store = this.owner.lookup('service:store'); + const Person = store.modelFor('person'); this.owner.register( 'adapter:person', Adapter.extend({ queryRecord(store, type, query) { assert.strictEqual(type, Person, 'the query method is called with the correct type'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', attributes: { name: 'Peter Wagenet' } }, }); }, @@ -64,12 +63,12 @@ module('integration/store/query-record - Query one record with a query hash', fu 'adapter:person', Adapter.extend({ queryRecord(store, type, query) { - return reject(); + return Promise.reject(); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.queryRecord('person', {}).catch(function (reason) { assert.ok(true, 'The rejection handler was called'); @@ -97,7 +96,7 @@ module('integration/store/query-record - Query one record with a query hash', fu 'adapter:person', Adapter.extend({ queryRecord(store, type, query) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -110,7 +109,7 @@ module('integration/store/query-record - Query one record with a query hash', fu }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.queryRecord('person', { related: 'posts' }); }); diff --git a/tests/main/tests/integration/store/query-test.js b/tests/main/tests/integration/store/query-test.js index 101b9803a67..f705cdd472a 100644 --- a/tests/main/tests/integration/store/query-test.js +++ b/tests/main/tests/integration/store/query-test.js @@ -1,10 +1,10 @@ import { module } from 'qunit'; -import RSVP from 'rsvp'; 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'; @@ -12,10 +12,10 @@ module('integration/store/query', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - const Person = Model.extend(); + class Person extends Model {} this.owner.register('model:person', Person); - this.owner.register('adapter:application', Adapter.extend()); + this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); @@ -23,20 +23,20 @@ module('integration/store/query', function (hooks) { 'meta is proxied correctly on the PromiseArray', { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, async function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let defered = RSVP.defer(); + const defered = createDeferred(); this.owner.register( 'adapter:person', - Adapter.extend({ + class extends Adapter { query(store, type, query) { return defered.promise; - }, - }) + } + } ); - let result = store.query('person', {}); + const result = store.query('person', {}); assert.notOk(result.meta?.foo, 'precond: meta is not yet set'); diff --git a/tests/main/tests/integration/store/serializer-for-test.js b/tests/main/tests/integration/store/serializer-for-test.js index 1cb6d0b9abc..a904ca77266 100644 --- a/tests/main/tests/integration/store/serializer-for-test.js +++ b/tests/main/tests/integration/store/serializer-for-test.js @@ -1,11 +1,10 @@ -import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; -import Store from '@ember-data/store'; - class TestAdapter { constructor(args) { Object.assign(this, args); @@ -37,13 +36,13 @@ module('integration/store - serializerFor', function (hooks) { let store; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; store = owner.lookup('service:store'); }); test('when no serializer is available we return null', async function (assert) { - let { owner } = this; + const { owner } = this; /* serializer:-default is the "last chance" fallback and is the json-api serializer which is re-exported as app/serializers/-default. @@ -63,7 +62,7 @@ module('integration/store - serializerFor', function (hooks) { }); test('we find and instantiate the application serializer', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; class AppSerializer extends TestSerializer { @@ -74,13 +73,13 @@ module('integration/store - serializerFor', function (hooks) { owner.register('serializer:application', AppSerializer); - let serializer = store.serializerFor('application'); + const serializer = store.serializerFor('application'); assert.ok(serializer instanceof AppSerializer, 'We found the correct serializer'); assert.ok(didInstantiate, 'We instantiated the serializer'); didInstantiate = false; - let serializerAgain = store.serializerFor('application'); + const serializerAgain = store.serializerFor('application'); assert.ok(serializerAgain instanceof AppSerializer, 'We found the correct serializer'); assert.notOk(didInstantiate, 'We did not instantiate the serializer again'); @@ -88,7 +87,7 @@ module('integration/store - serializerFor', function (hooks) { }); test('multiple stores do not share serializers', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; class AppSerializer extends TestSerializer { @@ -100,14 +99,14 @@ module('integration/store - serializerFor', function (hooks) { owner.register('serializer:application', AppSerializer); owner.register('service:other-store', Store); - let otherStore = owner.lookup('service:other-store'); - let serializer = store.serializerFor('application'); + const otherStore = owner.lookup('service:other-store'); + const serializer = store.serializerFor('application'); assert.ok(serializer instanceof AppSerializer, 'We found the correct serializer'); assert.ok(didInstantiate, 'We instantiated the serializer'); didInstantiate = false; - let otherSerializer = otherStore.serializerFor('application'); + const otherSerializer = otherStore.serializerFor('application'); assert.ok(otherSerializer instanceof AppSerializer, 'We found the correct serializer again'); assert.ok(didInstantiate, 'We instantiated the other serializer'); assert.notStrictEqual(otherSerializer, serializer, 'We have a different serializer instance'); @@ -116,7 +115,7 @@ module('integration/store - serializerFor', function (hooks) { }); test('we can find and instantiate per-type serializers', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiateAppSerializer = false; let didInstantiatePersonSerializer = false; @@ -135,20 +134,20 @@ module('integration/store - serializerFor', function (hooks) { owner.register('serializer:application', AppSerializer); owner.register('serializer:person', PersonSerializer); - let serializer = store.serializerFor('person'); + const serializer = store.serializerFor('person'); assert.ok(serializer instanceof PersonSerializer, 'We found the correct serializer'); assert.ok(didInstantiatePersonSerializer, 'We instantiated the person serializer'); assert.notOk(didInstantiateAppSerializer, 'We did not instantiate the application serializer'); - let appSerializer = store.serializerFor('application'); + const appSerializer = store.serializerFor('application'); assert.ok(appSerializer instanceof AppSerializer, 'We found the correct serializer'); assert.ok(didInstantiateAppSerializer, 'We instantiated the application serializer'); assert.notStrictEqual(appSerializer, serializer, 'We have separate serializers'); }); test('we fallback to the application serializer when a per-type serializer is not found', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiateAppSerializer = false; class AppSerializer extends TestSerializer { @@ -159,20 +158,20 @@ module('integration/store - serializerFor', function (hooks) { owner.register('serializer:application', AppSerializer); - let serializer = store.serializerFor('person'); + const serializer = store.serializerFor('person'); assert.ok(serializer instanceof AppSerializer, 'We found the serializer'); assert.ok(didInstantiateAppSerializer, 'We instantiated the serializer'); didInstantiateAppSerializer = false; - let appSerializer = store.serializerFor('application'); + const appSerializer = store.serializerFor('application'); assert.ok(appSerializer instanceof AppSerializer, 'We found the correct serializer'); assert.notOk(didInstantiateAppSerializer, 'We did not instantiate the serializer again'); assert.strictEqual(appSerializer, serializer, 'We fell back to the application serializer instance'); }); test('serializers are destroyed', async function (assert) { - let { owner } = this; + const { owner } = this; let didInstantiate = false; let didDestroy = false; @@ -188,12 +187,13 @@ module('integration/store - serializerFor', function (hooks) { owner.register('serializer:application', AppSerializer); - let serializer = store.serializerFor('application'); + const serializer = store.serializerFor('application'); assert.ok(serializer instanceof AppSerializer, 'precond - We found the correct serializer'); assert.ok(didInstantiate, 'precond - We instantiated the serializer'); - run(store, 'destroy'); + store.destroy(); + await settled(); assert.ok(didDestroy, 'serializer was destroyed'); }); diff --git a/tests/main/tests/integration/store/store-creation-recursion-test.js b/tests/main/tests/integration/store/store-creation-recursion-test.js index 2fe05764f8d..5d32a15774b 100644 --- a/tests/main/tests/integration/store/store-creation-recursion-test.js +++ b/tests/main/tests/integration/store/store-creation-recursion-test.js @@ -8,12 +8,12 @@ module('integration/store/creation-recursion', function (hooks) { setupTest(hooks); test('store construction does not construct transforms', function (assert) { - let storeFactory = this.owner.factoryFor('service:store'); + const storeFactory = this.owner.factoryFor('service:store'); this.owner.unregister('service:store'); this.owner.register('service:store', storeFactory.class); - let test = this; + const test = this; test.dateTransformCreated = false; class MockDateTransform extends Transform { constructor(...args) { diff --git a/tests/main/tests/test-helper.js b/tests/main/tests/test-helper.js index 716f0f152c4..a28ce2366a6 100644 --- a/tests/main/tests/test-helper.js +++ b/tests/main/tests/test-helper.js @@ -1,43 +1,187 @@ -import { setApplication } from '@ember/test-helpers'; +import { _backburner } from '@ember/runloop'; +import { getSettledState, isSettled, registerHook, setApplication } from '@ember/test-helpers'; +import { getPendingWaiterState } from '@ember/test-waiters'; import * as QUnit from 'qunit'; import { setup } from 'qunit-dom'; -import RSVP from 'rsvp'; -import { start } from 'ember-qunit'; +import start from 'ember-exam/test-support/start'; -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; -import customQUnitAdapter from '@ember-data/unpublished-test-infra/test-support/testem/custom-qunit-adapter'; +import { setBuildURLConfig } from '@ember-data/request-utils'; +import configureAsserts from '@ember-data/unpublished-test-infra/test-support/asserts/index'; +import { setConfig, setTestId } from '@warp-drive/holodeck'; import Application from '../app'; import config from '../config/environment'; -QUnit.dump.maxDepth = 3; -setup(QUnit.assert); +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; +setBuildURLConfig({ + host: MockHost, + namespace: '', +}); +setConfig({ host: MockHost }); -configureAsserts(); +QUnit.config.urlConfig.push( + { + id: 'debugMemory', + label: 'Enable Memory Debugging', + tooltip: 'Add instrumentation to capture memory stats after every test, reported to the DOT Reporter.', + }, + { + id: 'disableHtmlReporter', + label: 'Disable HTML Reporter for Headless CI', + tooltip: + 'Disable HTML Reporter output for individual test results, helps with large test suites and to help isolate leaks.', + }, + { id: 'debugSettled', label: 'Enable Settled State Debugging' }, + { id: 'debugPerformance', label: 'Enable Performance Instrumentation' }, + { id: 'debugPermissions', label: 'Permission Check Logging' }, + { id: 'debugBackburner', label: 'Enable Backburner debugging' }, + { id: 'enableA11yAudit', label: 'Enable A11y Audit' } +); -setApplication(Application.create(config.APP)); +if (window.location.search.includes('debugBackburner')) { + _backburner.DEBUG = true; +} -assertAllDeprecations(); +const { SHOW_SPANS, DEBUG_SETTLED_STATE, DEBUG_MEMORY, GC_BREATHE_TIME, DELAY_TEST_START } = window; -if (window.Testem) { - window.Testem.useCustomAdapter(customQUnitAdapter); +// useful for debugging test performance with the profiler +// activate and record a performance profile +if (SHOW_SPANS) { + let SPAN = 0; + [ + 'select', + 'render', + 'rerender', + 'typeIn', + 'findAll', + 'click', + 'focus', + 'fillIn', + 'blur', + 'waitFor', + 'waitUntil', + 'scrollTo', + 'settled', + 'visit', + ].forEach((helper) => { + let spanId; + registerHook(helper, 'start', function () { + spanId = SPAN++; + performance.mark(`${helper}-${spanId}-start`); + }); + registerHook(helper, 'end', function () { + performance.mark(`${helper}-${spanId}-end`); + performance.measure(`${helper}-${spanId}`, `${helper}-${spanId}-start`, `${helper}-${spanId}-end`); + }); + }); } -QUnit.begin(function () { - RSVP.configure('onerror', (reason) => { - // only print error messages if they're exceptions; - // otherwise, let a future turn of the event loop - // handle the error. - // TODO kill this off - if (reason && reason instanceof Error) { - throw reason; - } - }); +// useful for log-points in the debugger for `waitUntil`'s +// scheduleCheck etc. to determine why churning during a test +// +// Also useful for checking what's going on when the test-isolation check fails +Object.assign(window, { + getBackburnerInfo: () => { + return { + autoRun: _backburner._autorun, + timersCount: _backburner._timers?.length || 0, + timers: _backburner._timers?.slice() || [], + }; + }, + getSettledState, + getPendingWaiterState, }); +function setupMemoryTracking() { + // this is set above + // and works together with testem code in tests/index.html + // and our custom DOT Reporter + // you should also set DEBUG_MEMORY=true in your env + // to get granular memory data + if (DEBUG_MEMORY) { + if (DELAY_TEST_START) { + QUnit.begin(async function () { + await new Promise((resolve) => { + // give the GC time to breathe before the very first test + // to recover from initial boot + // also gives user a chance to manually GC before an initial heap snapshot + setTimeout(resolve, 8_000); + }); + }); + } + + QUnit.hooks.afterEach(async function (assert) { + if (DEBUG_SETTLED_STATE) { + if (!isSettled()) { + assert.ok(false, `Expected test to be settled after teardown`); + } + await new Promise((resolve) => { + setTimeout(resolve, 10); // try to catch anything still in flight + }); + if (!isSettled()) { + assert.ok(false, `Expected test to be settled after teardown`); + } + } + // if the gc is exposed, use it + if (typeof gc !== 'undefined') { + gc(); + } + + // we give more breathing time if the queue is done + // so that our final number has as much time to have GC'd as possible. + // note this wait time will not count towards the testTimeout; + // however, it will count towards the runDuration. + // Ideally we would wait longer for the very last test + // however, when load balancing the queue is drained often. + const WaitTime = GC_BREATHE_TIME; + if (WaitTime) { + await new Promise((resolve) => { + setTimeout(resolve, WaitTime); // give lots of breathing room for GC; + }); + } + + // so we use this "deprecated" api instead of the one that might not be available below + const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = performance.memory; + const data = { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize }; + // chrome claims this is available but it is not available, even for https + if (performance.measureUserAgentSpecificMemory) { + data.granular = await performance.measureUserAgentSpecificMemory(); + } + window.MEMORY_DATA[assert.test.testId] = data; + }); + } else if (DEBUG_SETTLED_STATE) { + QUnit.hooks.afterEach(async function (assert) { + if (!isSettled()) { + assert.ok(false, `Expected test to be settled after teardown`); + } + await new Promise((resolve) => { + setTimeout(resolve, 10); // try to catch anything still in flight + }); + if (!isSettled()) { + assert.ok(false, `Expected test to be settled after teardown`); + } + }); + } +} + +// memory tracking needs to be the very first hook setup +// so that it runs last. This also ensures we catch things +// that don't use our custom test helpers. +setupMemoryTracking(); +configureAsserts(QUnit.hooks); + QUnit.config.testTimeout = 2000; +QUnit.dump.maxDepth = 6; -start({ setupTestIsolationValidation: true }); +QUnit.hooks.beforeEach(function (assert) { + setTestId(assert.test.testId); +}); +QUnit.hooks.afterEach(function (assert) { + setTestId(null); +}); + +setup(QUnit.assert); +setApplication(Application.create(config.APP)); +start({ setupEmberOnerrorValidation: false, setupTestIsolationValidation: true }); diff --git a/tests/main/tests/unit/.gitkeep b/tests/main/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/main/tests/unit/adapter-errors-test.js b/tests/main/tests/unit/adapter-errors-test.js index f7d2fb69525..356d7979bfb 100644 --- a/tests/main/tests/unit/adapter-errors-test.js +++ b/tests/main/tests/unit/adapter-errors-test.js @@ -12,103 +12,103 @@ import AdapterError, { TimeoutError, UnauthorizedError, } from '@ember-data/adapter/error'; -import { DEPRECATE_HELPERS } from '@ember-data/deprecations'; 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) { - let error = new AdapterError(); + const error = new AdapterError(); - assert.ok(error instanceof Error); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'Adapter operation failed'); }); test('InvalidError', function (assert) { - let error = new InvalidError(); + const error = new InvalidError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter rejected the commit because it was invalid'); }); test('TimeoutError', function (assert) { - let error = new TimeoutError(); + const error = new TimeoutError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation timed out'); }); test('AbortError', function (assert) { - let error = new AbortError(); + const error = new AbortError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation was aborted'); }); test('UnauthorizedError', function (assert) { - let error = new UnauthorizedError(); + const error = new UnauthorizedError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation is unauthorized'); }); test('ForbiddenError', function (assert) { - let error = new ForbiddenError(); + const error = new ForbiddenError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation is forbidden'); }); test('NotFoundError', function (assert) { - let error = new NotFoundError(); + const error = new NotFoundError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter could not find the resource'); }); test('ConflictError', function (assert) { - let error = new ConflictError(); + const error = new ConflictError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation failed due to a conflict'); }); test('ServerError', function (assert) { - let error = new ServerError(); + const error = new ServerError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'The adapter operation failed due to a server error'); }); test('CustomAdapterError', function (assert) { - let CustomAdapterError = AdapterError.extend(); - let error = new CustomAdapterError(); + const CustomAdapterError = AdapterError.extend(); + const error = new CustomAdapterError(); - assert.ok(error instanceof Error); - assert.ok(error instanceof AdapterError); - assert.ok(error.isAdapterError); + assert.true(error instanceof Error, 'error is instanceof Error'); + assert.true(error instanceof AdapterError, 'error is instanceof AdapterError'); + assert.true(error.isAdapterError, 'error.isAdapterError'); assert.strictEqual(error.message, 'Adapter operation failed'); }); test('CustomAdapterError with default message', function (assert) { - let CustomAdapterError = AdapterError.extend({ message: 'custom error!' }); - let error = new CustomAdapterError(); + const CustomAdapterError = AdapterError.extend({ message: 'custom error!' }); + const error = new CustomAdapterError(); assert.strictEqual(error.message, 'custom error!'); }); @@ -155,25 +155,25 @@ module('unit/adapter-errors - AdapterError', function () { ]; test('errorsHashToArray', function (assert) { - let result = errorsHashToArray(errorsHash); + 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) { - let result = errorsHashToArray(errorsPrimaryHash); + 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) { - let result = errorsArrayToHash(errorsArray); + 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) { - let result = errorsArrayToHash([ + const result = errorsArrayToHash([ { detail: 'error message', source: { pointer: 'data/attributes/name' }, @@ -184,7 +184,7 @@ module('unit/adapter-errors - AdapterError', function () { }); test('errorsArrayToHash for primary data object', function (assert) { - let result = errorsArrayToHash(errorsPrimaryArray); + const result = errorsArrayToHash(errorsPrimaryArray); assert.deepEqual(result, errorsPrimaryHash); assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); }); diff --git a/tests/main/tests/unit/adapters/build-url-mixin/build-url-test.js b/tests/main/tests/unit/adapters/build-url-mixin/build-url-test.js index 0ad714ff4c0..a4cc99366c3 100644 --- a/tests/main/tests/unit/adapters/build-url-mixin/build-url-test.js +++ b/tests/main/tests/unit/adapters/build-url-mixin/build-url-test.js @@ -28,8 +28,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - find requestType delegates to urlForFindRecord', function (assert) { assert.expect(4); - let snapshotStub = { snapshot: true }; - let originalMethod = adapter.urlForFindRecord; + const snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForFindRecord; adapter.urlForFindRecord = function (id, type, snapshot) { assert.strictEqual(id, 1); assert.strictEqual(type, 'super-user'); @@ -41,8 +41,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - findAll requestType delegates to urlForFindAll', function (assert) { assert.expect(3); - let originalMethod = adapter.urlForFindAll; - let snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForFindAll; + const snapshotStub = { snapshot: true }; adapter.urlForFindAll = function (type, snapshot) { assert.strictEqual(type, 'super-user'); assert.strictEqual(snapshot, snapshotStub); @@ -53,8 +53,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - query requestType delegates to urlForQuery', function (assert) { assert.expect(3); - let originalMethod = adapter.urlForQuery; - let queryStub = { limit: 10 }; + const originalMethod = adapter.urlForQuery; + const queryStub = { limit: 10 }; adapter.urlForQuery = function (query, type) { assert.strictEqual(query, queryStub); assert.strictEqual(type, 'super-user'); @@ -65,8 +65,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - queryRecord requestType delegates to urlForQueryRecord', function (assert) { assert.expect(3); - let originalMethod = adapter.urlForQueryRecord; - let queryStub = { companyId: 10 }; + const originalMethod = adapter.urlForQueryRecord; + const queryStub = { companyId: 10 }; adapter.urlForQueryRecord = function (query, type) { assert.strictEqual(query, queryStub); assert.strictEqual(type, 'super-user'); @@ -77,8 +77,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - findMany requestType delegates to urlForFindMany', function (assert) { assert.expect(3); - let originalMethod = adapter.urlForFindMany; - let idsStub = [1, 2, 3]; + const originalMethod = adapter.urlForFindMany; + const idsStub = [1, 2, 3]; adapter.urlForFindMany = function (ids, type) { assert.strictEqual(ids, idsStub); assert.strictEqual(type, 'super-user'); @@ -89,8 +89,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - findHasMany requestType delegates to urlForFindHasMany', function (assert) { assert.expect(4); - let originalMethod = adapter.urlForFindHasMany; - let snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForFindHasMany; + const snapshotStub = { snapshot: true }; adapter.urlForFindHasMany = function (id, type, snapshot) { assert.strictEqual(id, 1); assert.strictEqual(type, 'super-user'); @@ -102,8 +102,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - findBelongsTo requestType delegates to urlForFindBelongsTo', function (assert) { assert.expect(4); - let originalMethod = adapter.urlForFindBelongsTo; - let snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForFindBelongsTo; + const snapshotStub = { snapshot: true }; adapter.urlForFindBelongsTo = function (id, type, snapshot) { assert.strictEqual(id, 1); assert.strictEqual(type, 'super-user'); @@ -115,8 +115,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - createRecord requestType delegates to urlForCreateRecord', function (assert) { assert.expect(3); - let snapshotStub = { snapshot: true }; - let originalMethod = adapter.urlForCreateRecord; + const snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForCreateRecord; adapter.urlForCreateRecord = function (type, snapshot) { assert.strictEqual(type, 'super-user'); assert.strictEqual(snapshot, snapshotStub); @@ -127,8 +127,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - updateRecord requestType delegates to urlForUpdateRecord', function (assert) { assert.expect(4); - let snapshotStub = { snapshot: true }; - let originalMethod = adapter.urlForUpdateRecord; + const snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForUpdateRecord; adapter.urlForUpdateRecord = function (id, type, snapshot) { assert.strictEqual(id, 1); assert.strictEqual(type, 'super-user'); @@ -140,8 +140,8 @@ module('unit/adapters/build-url-mixin/build-url - BuildURLMixin#buildURL', funct test('buildURL - deleteRecord requestType delegates to urlForDeleteRecord', function (assert) { assert.expect(4); - let snapshotStub = { snapshot: true }; - let originalMethod = adapter.urlForDeleteRecord; + const snapshotStub = { snapshot: true }; + const originalMethod = adapter.urlForDeleteRecord; adapter.urlForDeleteRecord = function (id, type, snapshot) { assert.strictEqual(id, 1); assert.strictEqual(type, 'super-user'); diff --git a/tests/main/tests/unit/adapters/build-url-mixin/path-for-type-test.js b/tests/main/tests/unit/adapters/build-url-mixin/path-for-type-test.js index cac2b5e00f4..9b245e0a938 100644 --- a/tests/main/tests/unit/adapters/build-url-mixin/path-for-type-test.js +++ b/tests/main/tests/unit/adapters/build-url-mixin/path-for-type-test.js @@ -9,7 +9,7 @@ module('unit/adapters/build-url-mixin/path-for-type - DS.BuildURLMixin#pathForTy hooks.beforeEach(function () { // test for overriden pathForType methods which return null path values - let customPathForType = { + const customPathForType = { pathForType(type) { if (type === 'rootModel') { return ''; @@ -22,17 +22,17 @@ module('unit/adapters/build-url-mixin/path-for-type - DS.BuildURLMixin#pathForTy }); test('pathForType - works with camelized types', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); assert.strictEqual(adapter.pathForType('superUser'), 'superUsers'); }); test('pathForType - works with dasherized types', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); assert.strictEqual(adapter.pathForType('super-user'), 'superUsers'); }); test('pathForType - works with underscored types', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); assert.strictEqual(adapter.pathForType('super_user'), 'superUsers'); }); }); diff --git a/tests/main/tests/unit/adapters/json-api-adapter/ajax-options-test.js b/tests/main/tests/unit/adapters/json-api-adapter/ajax-options-test.js index d0a99b1f497..47d7e60b0ac 100644 --- a/tests/main/tests/unit/adapters/json-api-adapter/ajax-options-test.js +++ b/tests/main/tests/unit/adapters/json-api-adapter/ajax-options-test.js @@ -19,12 +19,12 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() adds Accept when no other headers exist', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -36,14 +36,14 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() adds Accept header to existing headers', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = { 'Other-key': 'Other Value' }; - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -56,14 +56,14 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() adds Accept header to existing computed properties headers', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = { 'Other-key': 'Other Value' }; - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -76,14 +76,14 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() does not overwrite passed value of Accept headers', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = { 'Other-Key': 'Other Value', Accept: 'application/json' }; - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -96,14 +96,14 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() headers are set POST', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = {}; - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -116,14 +116,14 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() does not override with existing headers["Content-Type"] POST', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -136,17 +136,17 @@ module('unit/adapters/json-api-adapter/ajax-options - building requests', functi }); test('ajaxOptions() can override with options.contentType POST', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); adapter.headers = {}; - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { contentType: 'application/x-www-form-urlencoded', data: { type: 'post' }, }); - let receivedHeaders = ajaxOptions.headers; + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, diff --git a/tests/main/tests/unit/adapters/json-api-adapter/json-api-test.js b/tests/main/tests/unit/adapters/json-api-adapter/json-api-test.js index d6bfd31fd7a..e1736ba896e 100644 --- a/tests/main/tests/unit/adapters/json-api-adapter/json-api-test.js +++ b/tests/main/tests/unit/adapters/json-api-adapter/json-api-test.js @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; -module('unit/adapters/json-api-test', function () { +module('unit/adapters/json-api', function () { test('coalesceFindRequests default', function (assert) { const adapter = JSONAPIAdapter.extend(); assert.false(adapter.create().coalesceFindRequests, 'default result is false'); diff --git a/tests/main/tests/unit/adapters/rest-adapter/ajax-options-test.js b/tests/main/tests/unit/adapters/rest-adapter/ajax-options-test.js index c50fd42424a..a39e2aa7b85 100644 --- a/tests/main/tests/unit/adapters/rest-adapter/ajax-options-test.js +++ b/tests/main/tests/unit/adapters/rest-adapter/ajax-options-test.js @@ -1,7 +1,4 @@ -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; -import { Promise as EmberPromise, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -29,11 +26,11 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( this.owner.register('serializer:application', RESTSerializer.extend()); }); - test('When an id is searched, the correct url should be generated', function (assert) { + test('When an id is searched, the correct url should be generated', async function (assert) { assert.expect(2); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); let count = 0; @@ -45,41 +42,40 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( assert.strictEqual(url, '/places/1', 'should create the correct url'); } count++; - return resolve(); + return Promise.resolve(); }; - return run(() => { - return EmberPromise.all([adapter.findRecord(store, Person, 1, {}), adapter.findRecord(store, Place, 1, {})]); - }); + await adapter.findRecord(store, Person, '1', {}); + await adapter.findRecord(store, Place, '1', {}); }); - test(`id's should be sanatized`, function (assert) { + test(`id's should be sanatized`, async function (assert) { assert.expect(1); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.ajax = function (url, method) { assert.strictEqual(url, '/people/..%2Fplace%2F1', 'should create the correct url'); - return resolve(); + return Promise.resolve(); }; - return run(() => adapter.findRecord(store, Person, '../place/1', {})); + await adapter.findRecord(store, Person, '../place/1', {}); }); test('ajaxOptions() headers are set', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.headers = { 'Content-Type': 'application/json', 'Other-key': 'Other Value', }; - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); - let receivedHeaders = ajaxOptions.headers; + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); + const receivedHeaders = ajaxOptions.headers; assert.deepEqual( receivedHeaders, @@ -92,12 +88,12 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() do not serializes data when GET', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); assert.deepEqual(ajaxOptions, { credentials: 'same-origin', @@ -112,12 +108,12 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() serializes data when not GET', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); assert.deepEqual(ajaxOptions, { credentials: 'same-origin', @@ -133,12 +129,12 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() can provide own headers["Content-Type"]', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -159,12 +155,12 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() can provide own contentType in options', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { contentType: 'application/x-www-form-urlencoded', data: { key: 'value' }, }); @@ -184,12 +180,12 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() empty data', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); assert.deepEqual(ajaxOptions, { credentials: 'same-origin', @@ -201,16 +197,16 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() headers take precedence over adapter headers', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { headers: { 'Content-Type': 'application/json', }, @@ -227,23 +223,21 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); }); - test('_fetchRequest() returns a promise', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test('_fetchRequest() returns a promise', async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let noop = function () {}; + const noop = function () {}; - return run(() => { - let fetchPlacePromise = adapter._fetchRequest({ - url: '/places/1', - success: noop, - error: noop, - }); + const fetchPlacePromise = adapter._fetchRequest({ + url: '/places/1', + success: noop, + error: noop, + }); - assert.strictEqual(typeof fetchPlacePromise.then, 'function', '_fetchRequest does not return a promise'); + assert.strictEqual(typeof fetchPlacePromise.then, 'function', '_fetchRequest does not return a promise'); - return fetchPlacePromise; - }); + await fetchPlacePromise; }); module('ajax-options - ajax', function (hooks) { @@ -257,57 +251,57 @@ module('unit/adapters/rest-adapter/ajax-options - building requests', function ( }); test('ajaxOptions() Content-Type is not set with ajax GET', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'GET'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); + const url = 'example.com'; + const type = 'GET'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); assert.notOk(ajaxOptions.contentType, 'contentType not set with GET'); }); test('ajaxOptions() Content-Type is not set with ajax POST no data', function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, {}); + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, {}); assert.notOk(ajaxOptions.contentType, 'contentType not set with POST no data'); }); test('ajaxOptions() Content-Type is set with ajax POST with data if not useFetch', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); this.owner.register( 'adapter:application', class extends RESTAdapter { useFetch = false; } ); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); assert.strictEqual(ajaxOptions.contentType, 'application/json; charset=utf-8', 'contentType is set with POST'); }); test('ajaxOptions() Content-Type is set with ajax POST with data if useFetch', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); this.owner.register( 'adapter:application', class extends RESTAdapter { useFetch = true; } ); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); - let url = 'example.com'; - let type = 'POST'; - let ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); + const url = 'example.com'; + const type = 'POST'; + const ajaxOptions = adapter.ajaxOptions(url, type, { data: { type: 'post' } }); assert.strictEqual( ajaxOptions.headers['content-type'], diff --git a/tests/main/tests/unit/adapters/rest-adapter/detailed-message-test.js b/tests/main/tests/unit/adapters/rest-adapter/detailed-message-test.js index cad85fe5071..a130b098722 100644 --- a/tests/main/tests/unit/adapters/rest-adapter/detailed-message-test.js +++ b/tests/main/tests/unit/adapters/rest-adapter/detailed-message-test.js @@ -16,9 +16,9 @@ module('unit/adapters/rest_adapter/detailed_message_test - RESTAdapter#generated test('generating a wonderfully friendly error message should work', function (assert) { assert.expect(1); - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); - let friendlyMessage = adapter.generatedDetailedMessage( + const friendlyMessage = adapter.generatedDetailedMessage( 418, { 'content-type': 'text/plain' }, "I'm a little teapot, short and stout", @@ -39,9 +39,9 @@ module('unit/adapters/rest_adapter/detailed_message_test - RESTAdapter#generated }); test('generating a friendly error message with a missing content-type header should work', function (assert) { - let adapter = this.owner.lookup('adapter:application'); + const adapter = this.owner.lookup('adapter:application'); - let friendlyMessage = adapter.generatedDetailedMessage(418, {}, `I'm a little teapot, short and stout`, { + const friendlyMessage = adapter.generatedDetailedMessage(418, {}, `I'm a little teapot, short and stout`, { url: '/teapots/testing', method: 'GET', }); diff --git a/tests/main/tests/unit/adapters/rest-adapter/fetch-options-test.js b/tests/main/tests/unit/adapters/rest-adapter/fetch-options-test.js index 4c7f0cc2375..500dcdaed18 100644 --- a/tests/main/tests/unit/adapters/rest-adapter/fetch-options-test.js +++ b/tests/main/tests/unit/adapters/rest-adapter/fetch-options-test.js @@ -21,7 +21,7 @@ module('unit/adapters/rest-adapter/fetch-options', function (hooks) { data: dataAsObject, }; - let options = fetchOptions(undefinedQueryStringOptions); + const options = fetchOptions(undefinedQueryStringOptions); assert.deepEqual(options.body, '{"a":1,"c":3,"d":null,"e":0,"f":false}'); }); @@ -60,7 +60,7 @@ module('unit/adapters/rest-adapter/fetch-options', function (hooks) { data: stringifiedData, }; - let options = fetchOptions(optionsWithStringData); + const options = fetchOptions(optionsWithStringData); assert.strictEqual(options.body, stringifiedData); }); @@ -140,7 +140,7 @@ module('unit/adapters/rest-adapter/fetch-options', function (hooks) { test("fetchOptions sets the request body correctly when 'data' is a String", function (assert) { assert.expect(1); - let stringBody = JSON.stringify({ a: 1, b: 2, c: 3 }); + const stringBody = JSON.stringify({ a: 1, b: 2, c: 3 }); const postData = { url: 'https://emberjs.com', method: 'POST', @@ -167,7 +167,7 @@ module('unit/adapters/rest-adapter/fetch-options', function (hooks) { test("fetchOptions sets credentials when 'credentials' is not empty", function (assert) { assert.expect(1); - let credentials = 'include'; + const credentials = 'include'; const postData = { url: 'https://emberjs.com', method: 'POST', diff --git a/tests/main/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js b/tests/main/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js index 5b699e3dd20..1eb201150f8 100644 --- a/tests/main/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js +++ b/tests/main/tests/unit/adapters/rest-adapter/group-records-for-find-many-test.js @@ -1,7 +1,6 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { Promise as EmberPromise } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -38,18 +37,18 @@ module( ids: options.data.ids, }); - let queryString = options.data.ids + const queryString = options.data.ids .map((i) => { return 'ids%5B%5D=' + i; }) .join('&'); - let fullUrl = url + '?' + queryString; + const fullUrl = url + '?' + queryString; maxLength = this.maxURLLength; lengths.push(fullUrl.length); - let testRecords = options.data.ids.map((id) => ({ id })); - return EmberPromise.resolve({ testRecords: testRecords }); + const testRecords = options.data.ids.map((id) => ({ id })); + return Promise.resolve({ testRecords: testRecords }); } } @@ -61,9 +60,9 @@ module( }); test('groupRecordsForFindMany - findMany', async function (assert) { - let wait = []; + const wait = []; for (let i = 1; i <= 1024; i++) { - wait.push(store.findRecord('testRecord', i)); + wait.push(store.findRecord('test-record', String(i))); } assert.ok( @@ -74,9 +73,9 @@ module( }); test('groupRecordsForFindMany works for encodeURIComponent-ified ids', async function (assert) { - let wait = []; - wait.push(store.findRecord('testRecord', 'my-id:1')); - wait.push(store.findRecord('testRecord', 'my-id:2')); + const wait = []; + wait.push(store.findRecord('test-record', 'my-id:1')); + wait.push(store.findRecord('test-record', 'my-id:2')); await settled(); @@ -84,15 +83,15 @@ module( assert.strictEqual(requests[0].url, '/testRecords'); assert.deepEqual(requests[0].ids, ['my-id:1', 'my-id:2']); - await EmberPromise.all(wait); + await Promise.all(wait); }); test('_stripIDFromURL works with id being encoded - #4190', function (assert) { store._fetchManager = new FetchManager(store); - let record = store.createRecord('testRecord', { id: 'id:123' }); - let adapter = store.adapterFor('testRecord'); - let snapshot = store._fetchManager.createSnapshot(recordIdentifierFor(record)); - let strippedUrl = adapter._stripIDFromURL(store, snapshot); + const record = store.createRecord('test-record', { id: 'id:123' }); + const adapter = store.adapterFor('test-record'); + const snapshot = store._fetchManager.createSnapshot(recordIdentifierFor(record)); + const strippedUrl = adapter._stripIDFromURL(store, snapshot); assert.strictEqual(strippedUrl, '/testRecords/'); }); 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 53846b418f6..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 @@ -1,7 +1,4 @@ -import { TestContext } from '@ember/test-helpers'; - import { module, test } from 'qunit'; -import RSVP from 'rsvp'; import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; @@ -9,49 +6,43 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; -import { Cache } from '@ember-data/types/q/cache'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; -import type { SchemaService } from '@ember-data/types/q/schema-service'; -import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEBUG } from '@warp-drive/build-config/env'; +import type { Cache } from '@warp-drive/core-types/cache'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; + +import { TestSchema } from '../../utils/schema'; -module('unit/model - Custom Class Model', function (hooks) { - let store: Store; +module('unit/model - Custom Class Model', function (hooks: NestedHooks) { class Person { - constructor(public store: Store) { + declare store: CustomStore; + constructor(store: CustomStore) { this.store = store; } // these types aren't correct but we don't have a registry to help // make them correct yet - save(): Promise { - return this.store.saveRecord(this as unknown as RecordInstance); + save(): Promise { + return this.store.saveRecord(this); } } class CustomStore extends Store { - constructor(args: Record) { - super(args); - this.registerSchemaDefinitionService({ - attributesDefinitionFor() { - let schema: AttributesSchema = {}; - schema.name = { - kind: 'attribute', - options: {}, - type: 'string', + createSchemaService() { + const schema = new TestSchema(); + schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { name: 'name', - }; - return schema; - }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist() { - return true; - }, + kind: 'attribute', + type: null, + }, + ], }); + return schema; } - instantiateRecord(identifier, createOptions) { + // @ts-expect-error we are overriding this hook + instantiateRecord(identifier, createOptions): unknown { return new Person(this); } teardownRecord(record) {} @@ -59,25 +50,27 @@ module('unit/model - Custom Class Model', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register( 'adapter:application', - JSONAPIAdapter.extend({ - shouldBackgroundReloadRecord: () => false, - createRecord: () => RSVP.reject(), - }) + class extends JSONAPIAdapter { + shouldBackgroundReloadRecord = () => false; + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + createRecord = () => Promise.reject(); + } ); owner.register('serializer:application', JSONAPISerializer); + // @ts-expect-error missing type owner.unregister('service:store'); }); - test('notification manager', async function (assert) { + test('notification manager', function (assert) { assert.expect(7); let notificationCount = 0; - let identifier; + let identifier: StableRecordIdentifier; class CreationStore extends CustomStore { - instantiateRecord(id: StableRecordIdentifier, createRecordArgs): Object { + instantiateRecord(id: StableRecordIdentifier, createRecordArgs): object { identifier = id; this.notifications.subscribe(identifier, (passedId, key) => { notificationCount++; @@ -92,17 +85,19 @@ module('unit/model - Custom Class Model', function (hooks) { }); return { hi: 'igor' }; } + + teardownRecord(record) {} } this.owner.register('service:store', CreationStore); const store = this.owner.lookup('service:store') as Store; - const storeWrapper = store._instanceCache._storeWrapper; + const capabilities = store._instanceCache._storeWrapper; store.push({ data: { id: '1', type: 'person', attributes: { name: 'chris' } } }); // emulate this happening within a single push store._join(() => { - storeWrapper.notifyChange(identifier, 'relationships', 'key'); - storeWrapper.notifyChange(identifier, 'relationships', 'key'); - storeWrapper.notifyChange(identifier, 'state'); - storeWrapper.notifyChange(identifier, 'errors'); + capabilities.notifyChange(identifier, 'relationships', 'key'); + capabilities.notifyChange(identifier, 'relationships', 'key'); + capabilities.notifyChange(identifier, 'state'); + capabilities.notifyChange(identifier, 'errors'); }); assert.strictEqual(notificationCount, 3, 'called notification callback'); @@ -110,11 +105,11 @@ module('unit/model - Custom Class Model', function (hooks) { test('record creation and teardown', function (assert) { assert.expect(5); - let returnValue; + let returnValue: unknown; class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs) { + instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs) { assert.strictEqual(identifier.type, 'person', 'Identifier type passed in correctly'); - assert.deepEqual(createRecordArgs, { otherProp: 'unk' }, 'createRecordArg passed in'); + assert.deepEqual(createRecordArgs, { name: 'chris', otherProp: 'unk' }, 'createRecordArg passed in'); returnValue = {}; return returnValue; } @@ -123,75 +118,42 @@ module('unit/model - Custom Class Model', function (hooks) { } } this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; - let person = store.createRecord('person', { name: 'chris', otherProp: 'unk' }); + const store = this.owner.lookup('service:store') as Store; + const person = store.createRecord('person', { name: 'chris', otherProp: 'unk' }) as Record; assert.strictEqual(returnValue, person, 'createRecord returns the instantiated record'); assert.deepEqual(returnValue, person, 'record instantiating does not modify the returned value'); }); - deprecatedTest( - 'recordData lookup', - { id: 'ember-data:deprecate-instantiate-record-args', count: 1, until: '5.0' }, - function (this: TestContext, assert: Assert) { - assert.expect(1); - let rd; - class CreationStore extends Store { - // @ts-expect-error - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { - rd = recordDataFor(identifier); - assert.strictEqual(rd.getAttr(identifier, 'name'), 'chris', 'Can look up record data from recordDataFor'); - return {}; - } - teardownRecord(record) {} - } - this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaService = { - attributesDefinitionFor({ type: string }): AttributesSchema { - return { - name: { - type: 'string', - options: {}, - name: 'name', - kind: 'attribute', - }, - }; - }, - relationshipsDefinitionFor({ type: string }): RelationshipsSchema { - return {}; - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - - store.createRecord('person', { name: 'chris' }); - } - ); - - test('attribute and relationship with custom schema definition', async function (assert) { - assert.expect(18); + test('fields with custom schema definition', async function (assert) { this.owner.register( 'adapter:application', - JSONAPIAdapter.extend({ - shouldBackgroundReloadRecord: () => false, - createRecord: (store, type, snapshot: Snapshot) => { + class extends JSONAPIAdapter { + shouldBackgroundReloadRecord = () => false; + createRecord = (store, type, snapshot: Snapshot) => { let count = 0; + assert.verifySteps( + DEBUG + ? ['TestSchema:fields', 'TestSchema:fields', 'TestSchema:hasResource', 'TestSchema:hasResource'] + : ['TestSchema:fields', 'TestSchema:fields'], + 'serialization of record for save' + ); + assert.step('Adapter:createRecord'); snapshot.eachAttribute((attr, attrDef) => { if (count === 0) { + assert.step('Adapter:createRecord:attr:name'); assert.strictEqual(attr, 'name', 'attribute key is correct'); assert.deepEqual( attrDef, { kind: 'attribute', type: 'string', options: {}, name: 'name' }, - 'attribute def matches schem' + 'attribute def matches schema' ); } else if (count === 1) { + assert.step('Adapter:createRecord:attr:age'); assert.strictEqual(attr, 'age', 'attribute key is correct'); assert.deepEqual( attrDef, { kind: 'attribute', type: 'number', options: {}, name: 'age' }, - 'attribute def matches schem' + 'attribute def matches schema' ); } count++; @@ -199,6 +161,7 @@ module('unit/model - Custom Class Model', function (hooks) { count = 0; snapshot.eachRelationship((rel, relDef) => { if (count === 0) { + assert.step('Adapter:createRecord:rel:boats'); assert.strictEqual(rel, 'boats', 'relationship key is correct'); assert.deepEqual( relDef, @@ -207,107 +170,116 @@ module('unit/model - Custom Class Model', function (hooks) { kind: 'hasMany', options: { inverse: null, + async: false, }, name: 'boats', - key: 'boats', }, - 'relationships def matches schem' + 'relationships def matches schema' ); } else if (count === 1) { + assert.step('Adapter:createRecord:rel:house'); assert.strictEqual(rel, 'house', 'relationship key is correct'); assert.deepEqual( relDef, - { type: 'house', kind: 'belongsTo', options: { inverse: null }, key: 'house', name: 'house' }, - 'relationship def matches schem' + { + type: 'house', + kind: 'belongsTo', + options: { inverse: null, async: false }, + name: 'house', + }, + 'relationship def matches schema' ); } count++; }); - return RSVP.resolve({ data: { type: 'person', id: '1' } }); - }, - }) - ); - class CustomStore extends Store { - instantiateRecord(identifier, createOptions) { - return new Person(this); + assert.verifySteps([ + 'Adapter:createRecord', + 'TestSchema:fields', + 'Adapter:createRecord:attr:name', + 'Adapter:createRecord:attr:age', + 'TestSchema:fields', + 'Adapter:createRecord:rel:boats', + 'Adapter:createRecord:rel:house', + ]); + return Promise.resolve({ data: { type: 'person', id: '1' } }); + }; } - teardownRecord(record) {} - } + ); + this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaService = { - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - if (typeof identifier === 'string') { - assert.strictEqual(identifier, 'person', 'type passed in to the schema hooks'); - } else { - assert.strictEqual(identifier.type, 'person', 'type passed in to the schema hooks'); - } - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - age: { - type: 'number', - kind: 'attribute', - options: {}, - name: 'age', - }, - }; - }, - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - if (typeof identifier === 'string') { - assert.strictEqual(identifier, 'person', 'type passed in to the schema hooks'); - } else { - assert.strictEqual(identifier.type, 'person', 'type passed in to the schema hooks'); - } - return { - boats: { - type: 'ship', - kind: 'hasMany', - options: { - inverse: null, - }, - key: 'boats', - name: 'boats', + const store = this.owner.lookup('service:store') as CustomStore; + store.schema._assert = assert; + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'name', + }, + { + type: 'number', + kind: 'attribute', + options: {}, + name: 'age', + }, + { + type: 'ship', + kind: 'hasMany', + options: { + inverse: null, + async: false, }, - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - }, - key: 'house', - name: 'house', + name: 'boats', + }, + { + type: 'house', + kind: 'belongsTo', + options: { + inverse: null, + async: false, }, - }; - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - let person = store.createRecord('person', { name: 'chris' }); - await (person as unknown as Person).save(); + name: 'house', + }, + ], + }); + + assert.verifySteps(['TestSchema:registerResource'], 'initial population of schema'); + const person = store.createRecord('person', { name: 'chris' }) as Person; + assert.verifySteps(['TestSchema:fields', 'TestSchema:fields'], 'population of record on create'); + await person.save(); + assert.verifySteps( + DEBUG + ? [ + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:fields', + 'TestSchema:fields', + 'TestSchema:fields', + ] + : ['TestSchema:hasResource', 'TestSchema:fields', 'TestSchema:fields', 'TestSchema:fields'], + 'update of record on save completion' + ); }); test('store.saveRecord', async function (assert) { assert.expect(1); this.owner.register( 'adapter:application', - JSONAPIAdapter.extend({ - shouldBackgroundReloadRecord: () => false, - createRecord: (store, type, snapshot) => { - return RSVP.resolve({ data: { type: 'person', id: '7' } }); - }, - }) + class extends JSONAPIAdapter { + shouldBackgroundReloadRecord = () => false; + createRecord = (store, type, snapshot) => { + return Promise.resolve({ data: { type: 'person', id: '7' } }); + }; + } ); this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let person = store.createRecord('person', { name: 'chris' }); - let promisePerson = await store.saveRecord(person); + const store = this.owner.lookup('service:store') as Store; + const person = store.createRecord('person', { name: 'chris' }); + const promisePerson = await store.saveRecord(person); assert.strictEqual(person, promisePerson, 'save promise resolves with the same record'); }); @@ -316,20 +288,20 @@ module('unit/model - Custom Class Model', function (hooks) { assert.expect(10); this.owner.register( 'adapter:application', - JSONAPIAdapter.extend({ - shouldBackgroundReloadRecord: () => false, - deleteRecord: (store, type, snapshot) => { + class extends JSONAPIAdapter { + shouldBackgroundReloadRecord = () => false; + deleteRecord(store, type, snapshot) { assert.ok(true, 'adapter method called'); - return RSVP.resolve(); - }, - }) + return Promise.resolve({ data: null }); + } + } ); const subscribedValues: string[] = []; class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs) { + instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs) { ident = identifier; assert.false(this.cache.isDeleted(identifier), 'we are not deleted when we start'); - this.notifications.subscribe(identifier, (passedId, key) => { + this.notifications.subscribe(identifier, (passedId, key: string) => { subscribedValues.push(key); assert.true(this.cache.isDeleted(identifier), 'we have been marked as deleted'); }); @@ -342,11 +314,11 @@ module('unit/model - Custom Class Model', function (hooks) { this.owner.register('service:store', CreationStore); const store = this.owner.lookup('service:store') as Store; const rd: Cache = store.cache; - let person = store.push({ data: { type: 'person', id: '1', attributes: { name: 'chris' } } }); + const person = store.push({ data: { type: 'person', id: '1', attributes: { name: 'chris' } } }); store.deleteRecord(person); - assert.true(rd!.isDeleted(ident!), 'record has been marked as deleted'); + assert.true(rd.isDeleted(ident!), 'record has been marked as deleted'); await store.saveRecord(person); - assert.true(rd!.isDeletionCommitted(ident!), 'deletion has been commited'); + assert.true(rd.isDeletionCommitted(ident!), 'deletion has been committed'); assert.strictEqual(subscribedValues.length, 3, 'we received the proper notifications'); // TODO this indicates our implementation could likely be more efficient assert.deepEqual(subscribedValues, ['state', 'removed', 'state'], 'state change to deleted has been notified'); @@ -356,71 +328,54 @@ module('unit/model - Custom Class Model', function (hooks) { assert.expect(1); this.owner.register( 'adapter:application', - JSONAPIAdapter.extend({ - shouldBackgroundReloadRecord: () => false, - createRecord: (store, type, snapshot) => { - return RSVP.reject(); - }, - }) - ); - class CustomStore extends Store { - instantiateRecord(identifier, createOptions) { - return new Person(this); + class extends JSONAPIAdapter { + shouldBackgroundReloadRecord = () => false; + createRecord = (store, type, snapshot) => { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject(); + }; } - teardownRecord(record) {} - } + ); + this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaService = { - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - let modelName = (identifier as RecordIdentifier).type || identifier; - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - }, - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - let modelName = (identifier as RecordIdentifier).type || identifier; - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - async: true, - }, - key: 'house', - name: 'house', + const store = this.owner.lookup('service:store') as CustomStore; + store.schema.registerResources([ + { + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { + type: 'house', + kind: 'belongsTo', + options: { + inverse: null, + async: true, }, - }; - } else { - return {}; - } + name: 'house', + }, + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'name', + }, + ], }, - doesTypeExist() { - return true; + { + identity: { name: 'id', kind: '@id' }, + type: 'house', + fields: [ + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'address', + }, + ], }, - }; - store.registerSchemaDefinitionService(schema); - let person = store.push({ + ]); + + const person = store.push({ data: { type: 'person', id: '7', @@ -428,7 +383,7 @@ module('unit/model - Custom Class Model', function (hooks) { relationships: { house: { data: { type: 'house', id: '1' } } }, }, }); - let serialized = store.serializeRecord(person, { includeId: true }); + const serialized = store.serializeRecord(person, { includeId: true }); assert.deepEqual( { data: { @@ -451,158 +406,4 @@ module('unit/model - Custom Class Model', function (hooks) { 'serializes record correctly' ); }); - - /* - TODO determine if there's any validity to keeping these - tes('relationshipReferenceFor belongsTo', async function (assert) { - assert.expect(3); - this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: modelName }: { type: string }): AttributesSchema { - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - }, - relationshipsDefinitionFor({ type: modelName }: { type: string }): RelationshipsSchema { - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - }, - key: 'house', - name: 'house', - }, - }; - } else { - return {}; - } - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - store.push({ - data: { - type: 'house', - id: '1', - attributes: { address: 'boat' }, - }, - }); - let person = store.push({ - data: { - type: 'person', - id: '7', - attributes: { name: 'chris' }, - relationships: { house: { data: { type: 'house', id: '1' } } }, - }, - }); - let identifier = recordIdentifierFor(person); - let relationship = store.relationshipReferenceFor({ type: 'person', id: '7', lid: identifier.lid }, 'house'); - assert.strictEqual(relationship.id(), '1', 'house relationship id found'); - assert.strictEqual(relationship.type, 'house', 'house relationship type found'); - assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); - }); - - tes('relationshipReferenceFor hasMany', async function (assert) { - assert.expect(3); - this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: modelName }: { type: string }): AttributesSchema { - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - }, - relationshipsDefinitionFor({ type: modelName }: { type: string }): RelationshipsSchema { - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'hasMany', - options: { - inverse: null, - }, - key: 'house', - name: 'house', - }, - }; - } else { - return {}; - } - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - store.push({ - data: { - type: 'house', - id: '1', - attributes: { address: 'boat' }, - }, - }); - let person = store.push({ - data: { - type: 'person', - id: '7', - attributes: { name: 'chris' }, - relationships: { - house: { - data: [ - { type: 'house', id: '1' }, - { type: 'house', id: '2' }, - ], - }, - }, - }, - }); - let identifier = recordIdentifierFor(person); - let relationship = store.relationshipReferenceFor({ type: 'person', id: '7', lid: identifier.lid }, 'house'); - assert.deepEqual(relationship.ids(), ['1', '2'], 'relationship found'); - assert.strictEqual(relationship.type, 'house', 'house relationship type found'); - assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); - }); - */ }); diff --git a/tests/main/tests/unit/debug-test.js b/tests/main/tests/unit/debug-test.js index d32801fb75d..40d232813bf 100644 --- a/tests/main/tests/unit/debug-test.js +++ b/tests/main/tests/unit/debug-test.js @@ -1,144 +1,127 @@ -import { module, test } from 'qunit'; +// we can unskip these tests once we move them to something +// scoped to the @ember-data/debug package that can ensure +// the _debugInfo method is installed. +import { module, skip as test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { HAS_DEBUG_PACKAGE } from '@ember-data/packages'; // TODO move these tests to the @ember-data/debug package -if (HAS_DEBUG_PACKAGE) { - module('Debug', function (hooks) { - setupTest(hooks); - - test('_debugInfo groups the attributes and relationships correctly', function (assert) { - const MaritalStatus = Model.extend({ - name: attr('string'), - }); - - const Post = Model.extend({ - title: attr('string'), - }); - - const User = Model.extend({ - name: attr('string'), - isDrugAddict: attr('boolean'), - maritalStatus: belongsTo('marital-status', { async: false, inverse: null }), - posts: hasMany('post', { async: false, inverse: null }), - }); - - this.owner.register('model:marital-status', MaritalStatus); - this.owner.register('model:post', Post); - this.owner.register('model:user', User); - - let record = this.owner.lookup('service:store').createRecord('user'); - - let propertyInfo = record._debugInfo().propertyInfo; - - assert.strictEqual(propertyInfo.groups.length, 4); - assert.strictEqual(propertyInfo.groups[0].name, 'Attributes'); - assert.deepEqual(propertyInfo.groups[0].properties, ['id', 'name', 'isDrugAddict']); - assert.strictEqual(propertyInfo.groups[1].name, 'belongsTo'); - assert.deepEqual(propertyInfo.groups[1].properties, ['maritalStatus']); - assert.strictEqual(propertyInfo.groups[2].name, 'hasMany'); - assert.deepEqual(propertyInfo.groups[2].properties, ['posts']); +module('Debug', function (hooks) { + setupTest(hooks); + + test('_debugInfo groups the attributes and relationships correctly', function (assert) { + const MaritalStatus = Model.extend({ + name: attr('string'), }); - test('_debugInfo supports arbitray relationship types', async function (assert) { - class MaritalStatus extends Model { - @attr('string') name; - } + const Post = Model.extend({ + title: attr('string'), + }); - class Post extends Model { - @attr('string') title; - } + const User = Model.extend({ + name: attr('string'), + isDrugAddict: attr('boolean'), + maritalStatus: belongsTo('marital-status', { async: false, inverse: null }), + posts: hasMany('post', { async: false, inverse: null }), + }); - class User extends Model { - @attr('string') name; - @attr('boolean') isDrugAddict; - @belongsTo('marital-status', { async: false, inverse: null }) maritalStatus; - } + this.owner.register('model:marital-status', MaritalStatus); + this.owner.register('model:post', Post); + this.owner.register('model:user', User); - // posts: computed(() => [1, 2, 3]) - // .readOnly() - // .meta({ - // options: { inverse: null }, - // isRelationship: true, - // kind: 'customRelationship', - // name: 'posts', - // type: 'post', - // }), - - this.owner.register('model:marital-status', MaritalStatus); - this.owner.register('model:post', Post); - this.owner.register('model:user', User); - - const store = this.owner.lookup('service:store'); - - class SchemaDelegator { - constructor(schema) { - this._schema = schema; - } + const record = this.owner.lookup('service:store').createRecord('user'); - doesTypeExist(type) { - return this._schema.doesTypeExist(type); - } + const propertyInfo = record._debugInfo().propertyInfo; - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); - } + assert.strictEqual(propertyInfo.groups.length, 4); + assert.strictEqual(propertyInfo.groups[0].name, 'Attributes'); + assert.deepEqual(propertyInfo.groups[0].properties, ['id', 'name', 'isDrugAddict']); + assert.strictEqual(propertyInfo.groups[1].name, 'belongsTo'); + assert.deepEqual(propertyInfo.groups[1].properties, ['maritalStatus']); + assert.strictEqual(propertyInfo.groups[2].name, 'hasMany'); + assert.deepEqual(propertyInfo.groups[2].properties, ['posts']); + }); + + test('_debugInfo supports arbitrary relationship types', async function (assert) { + class MaritalStatus extends Model { + @attr('string') name; + } - relationshipsDefinitionFor(identifier) { - const sup = this._schema.relationshipsDefinitionFor(identifier); - if (identifier.type === 'user') { - return Object.assign(sup, { - posts: { + class Post extends Model { + @attr('string') title; + } + + class User extends Model { + @attr('string') name; + @attr('boolean') isDrugAddict; + @belongsTo('marital-status', { async: false, inverse: null }) maritalStatus; + } + + this.owner.register('model:marital-status', MaritalStatus); + this.owner.register('model:post', Post); + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store'); + + class SchemaDelegator { + constructor(schema) { + this._schema = schema; + } + + hasResource({ type }) { + return this._schema.hasResource({ type }); + } + + fields(identifier) { + const sup = this._schema.fields(identifier); + if (identifier.type === 'user') { + return new Map([ + [ + 'posts', + { kind: 'customRelationship', name: 'posts', type: 'post', options: { async: false, inverse: null }, }, - }); - } - return sup; + ], + ]); } + return sup; } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); - - const record = store.createRecord('user'); - const propertyInfo = record._debugInfo().propertyInfo; - - assert.deepEqual(propertyInfo, { - includeOtherProperties: true, - groups: [ - { - name: 'Attributes', - properties: ['id', 'name', 'isDrugAddict'], - expand: true, - }, - { - name: 'belongsTo', - properties: ['maritalStatus'], - expand: true, - }, - { - name: 'customRelationship', - properties: ['posts'], - expand: true, - }, - { - name: 'Flags', - properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], - }, - ], - expensiveProperties: ['maritalStatus', 'posts'], - }); - }); - }); -} else { - module('Debug Skipped', function () { - test('Alert Skipped', function (assert) { - assert.ok(false, 'Debug Tests were Skipped'); + } + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); + + const record = store.createRecord('user'); + const propertyInfo = record._debugInfo().propertyInfo; + + assert.deepEqual(propertyInfo, { + includeOtherProperties: true, + groups: [ + { + name: 'Attributes', + properties: ['id', 'name', 'isDrugAddict'], + expand: true, + }, + { + name: 'belongsTo', + properties: ['maritalStatus'], + expand: true, + }, + { + name: 'customRelationship', + properties: ['posts'], + expand: true, + }, + { + name: 'Flags', + properties: ['isLoaded', 'hasDirtyAttributes', 'isSaving', 'isDeleted', 'isError', 'isNew', 'isValid'], + }, + ], + expensiveProperties: ['maritalStatus', 'posts'], }); }); -} +}); diff --git a/tests/main/tests/unit/diff-array-test.js b/tests/main/tests/unit/diff-array-test.js deleted file mode 100644 index 10e47f8bfa9..00000000000 --- a/tests/main/tests/unit/diff-array-test.js +++ /dev/null @@ -1,487 +0,0 @@ -import { module, test } from 'qunit'; - -import { diffArray } from '@ember-data/model/-private'; - -const a = 'aaa'; -const b = 'bbb'; -const c = 'ccc'; -const d = 'ddd'; -const e = 'eee'; -const f = 'fff'; -const g = 'ggg'; -const h = 'hhh'; -const w = 'www'; -const x = 'xxx'; -const y = 'yyy'; -const z = 'zzz'; - -module('unit/diff-array Diff Array tests', function () { - test('diff array returns no change given two empty arrays', function (assert) { - const result = diffArray([], []); - assert.strictEqual(result.firstChangeIndex, null); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns no change given two identical arrays length 1', function (assert) { - const result = diffArray([a], [a]); - assert.strictEqual(result.firstChangeIndex, null); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns no change given two identical arrays length 3', function (assert) { - const result = diffArray([a, b, c], [a, b, c]); - assert.strictEqual(result.firstChangeIndex, null); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given one appended item with old length 0', function (assert) { - const result = diffArray([], [a]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given one appended item with old length 1', function (assert) { - const result = diffArray([a], [a, b]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given one appended item with old length 2', function (assert) { - const result = diffArray([a, b], [a, b, c]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given 3 appended items with old length 0', function (assert) { - const result = diffArray([], [a, b, c]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given 3 appended items with old length 1', function (assert) { - const result = diffArray([a], [a, b, c, d]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given 3 appended items with old length 2', function (assert) { - const result = diffArray([a, b], [a, b, c, d, e]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given one item removed from end with old length 1', function (assert) { - const result = diffArray([a], []); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item removed from end with old length 2', function (assert) { - const result = diffArray([a, b], [a]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item removed from end with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, b]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items removed from end with old length 3', function (assert) { - const result = diffArray([a, b, c], []); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items removed from end with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [a]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items removed from end with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, b]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item removed from beginning with old length 2', function (assert) { - const result = diffArray([a, b], [b]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item removed from beginning with old length 3', function (assert) { - const result = diffArray([a, b, c], [b, c]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items removed from beginning with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [d]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items removed from beginning with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [d, e]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item removed from middle with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, c]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item removed from middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, b, d, e]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items removed from middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, e]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items removed from middle with old length 7', function (assert) { - const result = diffArray([a, b, c, d, e, f, g], [a, b, f, g]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 0); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item added to middle with old length 2', function (assert) { - const result = diffArray([a, c], [a, b, c]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given one item added to middle with old length 4', function (assert) { - const result = diffArray([a, b, d, e], [a, b, c, d, e]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given 3 items added to middle with old length 2', function (assert) { - const result = diffArray([a, e], [a, b, c, d, e]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given 3 items added to middle with old length 4', function (assert) { - const result = diffArray([a, b, f, g], [a, b, c, d, e, f, g]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 0); - }); - - test('diff array returns correctly given complete replacement with length 1', function (assert) { - const result = diffArray([a], [b]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given complete replacement with length 3', function (assert) { - const result = diffArray([a, b, c], [x, y, z]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given complete replacement with longer length', function (assert) { - const result = diffArray([a, b], [x, y, z]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given one item replaced in middle with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, x, c]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced in middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, b, x, d, e]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced in middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, x, y, z, e]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced in middle with old length 7', function (assert) { - const result = diffArray([a, b, c, d, e, f, g], [a, b, x, y, z, f, g]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item replaced at beginning with old length 2', function (assert) { - const result = diffArray([a, b], [x, b]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced at beginning with old length 3', function (assert) { - const result = diffArray([a, b, c], [x, b, c]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced at beginning with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [x, y, z, d]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced at beginning with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [x, y, z, d, e, f]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item replaced at end with old length 2', function (assert) { - const result = diffArray([a, b], [a, x]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced at end with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, b, x]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced at end with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [a, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced at end with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [a, b, c, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 3); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item replaced with two in middle with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, x, y, c]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced with two in middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, b, x, y, d, e]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced with 4 in middle with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, w, x, y, z, e]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced with 4 in middle with old length 7', function (assert) { - const result = diffArray([a, b, c, d, e, f, g], [a, b, w, x, y, z, f, g]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item replaced with two at beginning with old length 2', function (assert) { - const result = diffArray([a, b], [x, y, b]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced with two at beginning with old length 3', function (assert) { - const result = diffArray([a, b, c], [x, y, b, c]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced with 4 at beginning with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [w, x, y, z, d]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced with 4 at beginning with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [w, x, y, z, d, e, f]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given one item replaced with two at end with old length 2', function (assert) { - const result = diffArray([a, b], [a, x, y]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given one item replaced with two at end with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, b, x, y]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 2); - assert.strictEqual(result.removedCount, 1); - }); - - test('diff array returns correctly given 3 items replaced with 4 at end with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [a, w, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given 3 items replaced with 4 at end with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [a, b, c, w, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 3); - assert.strictEqual(result.addedCount, 4); - assert.strictEqual(result.removedCount, 3); - }); - - test('diff array returns correctly given two items replaced with one in middle with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [a, x, d]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given two items replaced with one in middle with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [a, b, x, e, f]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given 4 items replaced with 3 in middle with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [a, x, y, z, f]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given 4 items replaced with 3 in middle with old length 8', function (assert) { - const result = diffArray([a, b, c, d, e, f, g, h], [a, b, x, y, z, g, h]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given two items replaced with one at beginning with old length 3', function (assert) { - const result = diffArray([a, b, c], [x, c]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given two items replaced with one at beginning with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [x, c, d]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given 4 items replaced with 3 at beginning with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [x, y, z, e]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given 4 items replaced with 3 at beginning with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [x, y, z, e, f]); - assert.strictEqual(result.firstChangeIndex, 0); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given two items replaced with one at end with old length 3', function (assert) { - const result = diffArray([a, b, c], [a, x]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given two items replaced with one at end with old length 4', function (assert) { - const result = diffArray([a, b, c, d], [a, b, x]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 1); - assert.strictEqual(result.removedCount, 2); - }); - - test('diff array returns correctly given 4 items replaced with 3 at end with old length 5', function (assert) { - const result = diffArray([a, b, c, d, e], [a, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given 4 items replaced with 3 at end with old length 6', function (assert) { - const result = diffArray([a, b, c, d, e, f], [a, b, x, y, z]); - assert.strictEqual(result.firstChangeIndex, 2); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 4); - }); - - test('diff array returns correctly given non-contiguous insertion', function (assert) { - const result = diffArray([a, c, e], [a, b, c, d, e]); - assert.strictEqual(result.firstChangeIndex, 1); - assert.strictEqual(result.addedCount, 3); - assert.strictEqual(result.removedCount, 1); - }); -}); diff --git a/tests/main/tests/unit/legacy-compat/formatted-id-test.js b/tests/main/tests/unit/legacy-compat/formatted-id-test.js new file mode 100644 index 00000000000..6e76d942d9e --- /dev/null +++ b/tests/main/tests/unit/legacy-compat/formatted-id-test.js @@ -0,0 +1,54 @@ +import { module } from 'qunit'; + +import { formattedId } from '@ember-data/legacy-compat/utils'; +import test from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; + +module('Unit | Data Utils | ID | formattedId (util)', function () { + test('it normalizes id as expected', function (assert) { + assert.strictEqual(formattedId(1), '1', `normalized 1 correctly`); + assert.strictEqual(formattedId('1'), '1', `normalized '1' correctly`); + assert.strictEqual(formattedId(null), null, `normalized null correctly`); + }); + + test('it throws an error when the id is undefined', function (assert) { + assert.throws(() => { + formattedId(); + }, /Error: formattedId: id must not be undefined/); + }); + + test('it throws an error when the id is empty', function (assert) { + assert.throws(() => { + formattedId(''); + }, /Error: formattedId: id must not be empty/); + }); + + test('it throws an error when the id is 0', function (assert) { + assert.throws(() => { + formattedId(0); + }, /Error: formattedId: id must not be 0/); + }); + + test('it throws an error when the id is "0"', function (assert) { + assert.throws(() => { + formattedId('0'); + }, /Error: formattedId: id must not be 0/); + }); + + test('it throws an error when the id is not a string', function (assert) { + assert.throws(() => { + formattedId(new Date()); + }, /Error: formattedId: id must be a number, string or null/); + + assert.throws(() => { + formattedId([]); + }, /Error: formattedId: id must be a number, string or null/); + + assert.throws(() => { + formattedId(true); + }, /Error: formattedId: id must be a number, string or null/); + + assert.throws(() => { + formattedId(false); + }, /Error: formattedId: id must be a number, string or null/); + }); +}); diff --git a/tests/main/tests/unit/legacy-compat/formatted-type-test.js b/tests/main/tests/unit/legacy-compat/formatted-type-test.js new file mode 100644 index 00000000000..e4923d36c6e --- /dev/null +++ b/tests/main/tests/unit/legacy-compat/formatted-type-test.js @@ -0,0 +1,49 @@ +import { module } from 'qunit'; + +import { configureTypeNormalization, formattedType } from '@ember-data/legacy-compat/utils'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import test from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; + +configureTypeNormalization((type) => dasherize(singularize(type))); + +module('Unit | Data Utils | Type | formattedType (util)', function () { + test('it normalizes types as expected', function (assert) { + assert.expect(10); + ['post-comment', 'post-comments', 'PostComments', 'postComments', 'Post_comment', 'Post_Comments'].forEach( + (type) => { + assert.strictEqual(formattedType(type), 'post-comment', `normalized ${type} correctly`); + } + ); + ['post', 'posts', 'Post', 'Posts'].forEach((type) => { + assert.strictEqual(formattedType(type), 'post'); + }); + }); + + test('it throws an error when the type is null', function (assert) { + assert.throws(() => { + formattedType(null); + }, /Error: formattedType: type must not be null/); + }); + + test('it throws an error when the type is undefined', function (assert) { + assert.throws(() => { + formattedType(); + }, /Error: formattedType: type must not be undefined/); + }); + + test('it throws an error when the type is empty', function (assert) { + assert.throws(() => { + formattedType(''); + }, /Error: formattedType: type must not be empty/); + }); + + test('it throws an error when the type is not a string', function (assert) { + assert.throws(() => { + formattedType(new Date()); + }, /Error: formattedType: type must be a string/); + + assert.throws(() => { + formattedType([]); + }, /Error: formattedType: type must be a string/); + }); +}); diff --git a/tests/main/tests/unit/legacy-compat/is-equiv-id-test.js b/tests/main/tests/unit/legacy-compat/is-equiv-id-test.js new file mode 100644 index 00000000000..f85166058e4 --- /dev/null +++ b/tests/main/tests/unit/legacy-compat/is-equiv-id-test.js @@ -0,0 +1,74 @@ +import { module } from 'qunit'; + +import { isEquivId } from '@ember-data/legacy-compat/utils'; +import test from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; + +module('Unit | Data Utils | ID | isEquivId (util)', function () { + test('it compares ids as expected', function (assert) { + assert.true(isEquivId('1', 1), `compared '1' to 1 correctly`); + assert.true(isEquivId(1, '1'), `compared 1 to '1' correctly`); + assert.true(isEquivId(1, 1), `compared 1 to 1 correctly`); + assert.true(isEquivId('1', '1'), `compared '1' to '1' correctly`); + + assert.false(isEquivId('1', null), `compared '1' to null correctly`); + assert.false(isEquivId('1', 2), `compared '1' to 2 correctly`); + assert.false(isEquivId('1', '3'), `compared '1' to '3' correctly`); + }); + + test('it throws an error when expected id is null', function (assert) { + assert.throws(() => { + isEquivId(null, '1'); + }, /Error: isEquivId: Expected id must not be null/); + }); + + test('it throws an error when id is undefined', function (assert) { + assert.throws(() => { + isEquivId(undefined, '1'); + }, /Error: isEquivId: Expected id must not be undefined/); + assert.throws(() => { + isEquivId('post', undefined); + }, /Error: isEquivId: Actual id must not be undefined/); + }); + + test('it throws an error when the id is empty', function (assert) { + assert.throws(() => { + isEquivId('', '1'); + }, /Error: isEquivId: Expected id must not be empty/); + assert.throws(() => { + isEquivId('1', ''); + }, /Error: isEquivId: Actual id must not be empty/); + }); + + test('it throws an error when the id is 0', function (assert) { + assert.throws(() => { + isEquivId(0, '1'); + }, /Error: isEquivId: Expected id must not be 0/); + assert.throws(() => { + isEquivId('0', '1'); + }, /Error: isEquivId: Expected id must not be 0/); + assert.throws(() => { + isEquivId('1', 0); + }, /Error: isEquivId: Actual id must not be 0/); + assert.throws(() => { + isEquivId('1', '0'); + }, /Error: isEquivId: Actual id must not be 0/); + }); + + test('it throws an error when the id is not a string', function (assert) { + assert.throws(() => { + isEquivId(new Date(), '1'); + }, /Error: isEquivId: Expected id must be a number or string/); + + assert.throws(() => { + isEquivId([], '1'); + }, /Error: isEquivId: Expected id must be a number or string/); + + assert.throws(() => { + isEquivId('1', new Date()); + }, /Error: isEquivId: Actual id must be a number, string or null/); + + assert.throws(() => { + isEquivId('1', []); + }, /Error: isEquivId: Actual id must be a number, string or null/); + }); +}); diff --git a/tests/main/tests/unit/legacy-compat/is-equiv-type-test.js b/tests/main/tests/unit/legacy-compat/is-equiv-type-test.js new file mode 100644 index 00000000000..7dc471e69ab --- /dev/null +++ b/tests/main/tests/unit/legacy-compat/is-equiv-type-test.js @@ -0,0 +1,83 @@ +import { module } from 'qunit'; + +import { configureTypeNormalization, isEquivType } from '@ember-data/legacy-compat/utils'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import test from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; + +configureTypeNormalization((type) => dasherize(singularize(type))); + +module('Unit | Data Utils | Type | isEquivType (util)', function () { + test('it compares types as expected', function (assert) { + const passingPostCommentChecks = [ + 'post-comment', + 'post-comments', + 'PostComments', + 'postComments', + 'Post_comment', + 'Post_Comments', + ]; + const failingPostCommentChecks = ['post', 'post-comment-like']; + const passingPostChecks = ['post', 'posts', 'Post', 'Posts']; + assert.expect( + passingPostCommentChecks.length * 2 + passingPostChecks.length * 2 + failingPostCommentChecks.length * 2 + ); + + passingPostCommentChecks.forEach((type) => { + assert.true(isEquivType('post-comment', type), `compared ${type} to 'post-comment' correctly`); + assert.true(isEquivType(type, 'post-comment'), `compared 'post-comment' to ${type} correctly`); + }); + passingPostChecks.forEach((type) => { + assert.true(isEquivType('post', type), `compared ${type} to 'post' correctly`); + assert.true(isEquivType(type, 'post'), `compared 'post' to ${type} correctly`); + }); + failingPostCommentChecks.forEach((type) => { + assert.false(isEquivType('post-comment', type), `compared ${type} to 'post-comment' correctly`); + assert.false(isEquivType(type, 'post-comment'), `compared 'post-comment' to ${type} correctly`); + }); + }); + + test('it throws an error when type is null', function (assert) { + assert.throws(() => { + isEquivType(null, 'post'); + }, /Error: isEquivType: Expected type must not be null/); + assert.throws(() => { + isEquivType('post', null); + }, /Error: isEquivType: Actual type must not be null/); + }); + + test('it throws an error when type is undefined', function (assert) { + assert.throws(() => { + isEquivType(undefined, 'post'); + }, /Error: isEquivType: Expected type must not be undefined/); + assert.throws(() => { + isEquivType('post', undefined); + }, /Error: isEquivType: Actual type must not be undefined/); + }); + + test('it throws an error when the type is empty', function (assert) { + assert.throws(() => { + isEquivType('', 'post'); + }, /Error: isEquivType: Expected type must not be empty/); + assert.throws(() => { + isEquivType('post', ''); + }, /Error: isEquivType: Actual type must not be empty/); + }); + + test('it throws an error when the type is not a string', function (assert) { + assert.throws(() => { + isEquivType(new Date(), 'post'); + }, /Error: isEquivType: Expected type must be a string/); + + assert.throws(() => { + isEquivType([], 'post'); + }, /Error: isEquivType: Expected type must be a string/); + + assert.throws(() => { + isEquivType('post', new Date()); + }, /Error: isEquivType: Actual type must be a string/); + + assert.throws(() => { + isEquivType('post', []); + }, /Error: isEquivType: Actual type must be a string/); + }); +}); diff --git a/tests/main/tests/unit/many-array-test.js b/tests/main/tests/unit/many-array-test.js index 1540112978a..2dc76d2e83c 100644 --- a/tests/main/tests/unit/many-array-test.js +++ b/tests/main/tests/unit/many-array-test.js @@ -1,5 +1,4 @@ import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -26,7 +25,7 @@ module('unit/many_array - ManyArray', function (hooks) { const store = this.owner.lookup('service:store'); store.saveRecord = function (record) { assert.ok(true, 'record.save() was called'); - return resolve(); + return Promise.resolve(); }; store.push({ @@ -63,7 +62,7 @@ module('unit/many_array - ManyArray', function (hooks) { ], }); - let post = store.peekRecord('post', 3); + const post = store.peekRecord('post', 3); await post.tags.save().then(() => { assert.ok(true, 'manyArray.save() promise resolved'); diff --git a/tests/main/tests/unit/model-test.js b/tests/main/tests/unit/model-test.js index c6f4e40234b..5102cb856e9 100644 --- a/tests/main/tests/unit/model-test.js +++ b/tests/main/tests/unit/model-test.js @@ -1,7 +1,6 @@ import { computed, get, observer, set } from '@ember/object'; import { module, test } from 'qunit'; -import { reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -19,7 +18,7 @@ module('unit/model - Model', function (hooks) { let store, adapter; hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; class Person extends Model { @attr('string') @@ -46,7 +45,7 @@ module('unit/model - Model', function (hooks) { module('currentState', function () { test('supports pushedData in root.deleted.uncommitted', async function (assert) { - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -71,10 +70,10 @@ module('unit/model - Model', function (hooks) { test('supports canonical updates via pushedData in root.deleted.saved', async function (assert) { adapter.deleteRecord = () => { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }; - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -116,10 +115,10 @@ module('unit/model - Model', function (hooks) { testInDebug('Does not support dirtying in root.deleted.saved', async function (assert) { adapter.deleteRecord = () => { - return resolve({ data: null }); + return Promise.resolve({ data: null }); }; - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -158,7 +157,7 @@ module('unit/model - Model', function (hooks) { }); test('currentState is accessible when the record is created', async function (assert) { - let record = store.push({ + const record = store.push({ data: { type: 'person', id: '1', @@ -182,13 +181,13 @@ module('unit/model - Model', function (hooks) { }, }); - let record = await store.findRecord('person', '1'); + const record = await store.findRecord('person', '1'); assert.strictEqual(get(record, 'id'), '1', 'reports id as id by default'); }); test("a record's id is included in its toString representation", async function (assert) { - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -209,7 +208,7 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:test-model', TestModel); assert.expectAssertion(() => { - let ModelClass = store.modelFor('test-model'); + const ModelClass = store.modelFor('test-model'); get(ModelClass, 'attributes'); }, /You may not set `id` as an attribute on your model/); @@ -237,7 +236,7 @@ module('unit/model - Model', function (hooks) { // such as `watch` which is particularly problematic assert.expect(1); - let hasWatchMethod = Object.prototype.watch; + const hasWatchMethod = Object.prototype.watch; try { if (!hasWatchMethod) { Object.prototype.watch = function () {}; @@ -250,7 +249,7 @@ module('unit/model - Model', function (hooks) { }, }); - let record = await store.findRecord('person', 'watch'); + const record = await store.findRecord('person', 'watch'); assert.strictEqual(get(record, 'id'), 'watch', 'record is successfully created and could be found by its id'); } finally { @@ -294,17 +293,17 @@ module('unit/model - Model', function (hooks) { }); test('setting the id during createRecord should correctly update the id', async function (assert) { - let person = store.createRecord('person', { id: 'john' }); + const person = store.createRecord('person', { id: 'john' }); assert.strictEqual(person.id, 'john', 'new id should be correctly set.'); - let record = store.peekRecord('person', 'john'); + const record = store.peekRecord('person', 'john'); assert.strictEqual(person, record, 'The cache has an entry for john'); }); test('setting the id after createRecord should correctly update the id', async function (assert) { - let person = store.createRecord('person'); + const person = store.createRecord('person'); assert.strictEqual(person.id, null, 'initial created model id should be null'); @@ -312,25 +311,25 @@ module('unit/model - Model', function (hooks) { assert.strictEqual(person.id, 'john', 'new id should be correctly set.'); - let record = store.peekRecord('person', 'john'); + const record = store.peekRecord('person', 'john'); assert.strictEqual(person, record, 'The cache has an entry for john'); }); testInDebug('mutating the id after createRecord but before save works', async function (assert) { - let person = store.createRecord('person', { id: 'chris' }); + const person = store.createRecord('person', { id: 'chris' }); assert.strictEqual(person.id, 'chris', 'initial created model id should be null'); try { person.set('id', 'john'); assert.ok(false, 'we should have thrown an error during mutation'); - } catch (e) { + } catch { assert.ok(true, 'we did throw'); } - let chris = store.peekRecord('person', 'chris'); - let john = store.peekRecord('person', 'john'); + const chris = store.peekRecord('person', 'chris'); + const john = store.peekRecord('person', 'john'); assert.ok(chris === person, 'The cache still has an entry for chris'); assert.ok(john === null, 'The cache has no entry for john'); @@ -345,7 +344,7 @@ module('unit/model - Model', function (hooks) { }); this.owner.register('model:odd-person', OddPerson); - let person = store.createRecord('odd-person'); + const person = store.createRecord('odd-person'); let oddId = person.idComputed; assert.strictEqual(oddId, null, 'initial computed get is null'); @@ -374,7 +373,7 @@ module('unit/model - Model', function (hooks) { // we peek it instead of getting the return of push to make sure // we can locate it in the identity map - let record = store.peekRecord('person', 0); + const record = store.peekRecord('person', 0); assert.strictEqual(record.name, 'Tom Dale', 'found record with id 0'); }); @@ -393,8 +392,8 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:native-tag', NativeTag); this.owner.register('model:legacy-tag', LegacyTag); - let nativeTag = store.createRecord('native-tag', { name: 'test native' }); - let legacyTag = store.createRecord('legacy-tag', { name: 'test legacy' }); + const nativeTag = store.createRecord('native-tag', { name: 'test native' }); + const legacyTag = store.createRecord('legacy-tag', { name: 'test legacy' }); assert.strictEqual(get(nativeTag, 'name'), 'test native', 'the value is persisted'); assert.strictEqual(get(legacyTag, 'name'), 'test legacy', 'the value is persisted'); @@ -412,8 +411,8 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:native-tag', NativeTag); this.owner.register('model:legacy-tag', LegacyTag); - let nativeTag = store.createRecord('native-tag'); - let legacyTag = store.createRecord('legacy-tag'); + const nativeTag = store.createRecord('native-tag'); + const legacyTag = store.createRecord('legacy-tag'); assert.strictEqual(get(nativeTag, 'name'), 'unknown native tag', 'the default value is found'); assert.strictEqual(get(legacyTag, 'name'), 'unknown legacy tag', 'the default value is found'); @@ -430,7 +429,7 @@ module('unit/model - Model', function (hooks) { } this.owner.register('model:tag', Tag); - let tag = store.createRecord('tag'); + const tag = store.createRecord('tag'); assert.strictEqual(get(tag, 'createdAt'), 'le default value', 'the defaultValue function is evaluated'); }); @@ -448,7 +447,7 @@ module('unit/model - Model', function (hooks) { } this.owner.register('model:tag', Tag); - let tag = store.createRecord('tag'); + const tag = store.createRecord('tag'); get(tag, 'createdAt'); }); @@ -460,7 +459,7 @@ module('unit/model - Model', function (hooks) { } this.owner.register('model:tag', Tag); - let tag = store.createRecord('tag'); + const tag = store.createRecord('tag'); assert.expectAssertion(() => { get(tag, 'tagInfo'); @@ -471,7 +470,7 @@ module('unit/model - Model', function (hooks) { module('Attribute Transforms', function () { function converts(testName, type, provided, expected, options = {}) { test(testName, async function (assert) { - let { owner } = this; + const { owner } = this; class TestModel extends Model { @attr(type, options) name; @@ -482,7 +481,7 @@ module('unit/model - Model', function (hooks) { store.push(store.normalize('model', { id: '1', name: provided })); store.push(store.normalize('model', { id: '2' })); - let record = store.peekRecord('model', 1); + const record = store.peekRecord('model', 1); assert.deepEqual(get(record, 'name'), expected, type + ' coerces ' + provided + ' to ' + expected); }); @@ -490,7 +489,7 @@ module('unit/model - Model', function (hooks) { function convertsFromServer(testName, type, provided, expected) { test(testName, async function (assert) { - let { owner } = this; + const { owner } = this; class TestModel extends Model { @attr(type) name; @@ -499,7 +498,7 @@ module('unit/model - Model', function (hooks) { owner.register('model:model', TestModel); owner.register('serializer:model', JSONSerializer); - let record = store.push( + const record = store.push( store.normalize('model', { id: '1', name: provided, @@ -512,7 +511,7 @@ module('unit/model - Model', function (hooks) { function convertsWhenSet(testName, type, provided, expected) { test(testName, async function (assert) { - let { owner } = this; + const { owner } = this; class TestModel extends Model { @attr(type) name; @@ -521,7 +520,7 @@ module('unit/model - Model', function (hooks) { owner.register('model:model', TestModel); owner.register('serializer:model', JSONSerializer); - let record = store.push({ + const record = store.push({ data: { type: 'model', id: '2', @@ -569,8 +568,8 @@ module('unit/model - Model', function (hooks) { converts('null-to-null', 'date', null, null); converts('undefined-to-undefined', 'date', undefined, undefined); - let dateString = '2011-12-31T00:08:16.000Z'; - let date = new Date(dateString); + const dateString = '2011-12-31T00:08:16.000Z'; + const date = new Date(dateString); convertsFromServer('string-to-Date', 'date', dateString, date); convertsWhenSet('Date-to-string', 'date', date, dateString); @@ -579,7 +578,7 @@ module('unit/model - Model', function (hooks) { module('Reserved Props', function () { testInDebug(`don't allow setting of readOnly state props`, async function (assert) { - let record = store.createRecord('person'); + const record = store.createRecord('person'); if (navigator.userAgent.includes('Firefox/')) { assert.expectAssertion(() => { @@ -603,7 +602,7 @@ module('unit/model - Model', function (hooks) { }; function testReservedProperty(prop) { - let testName = `A subclass of Model cannot use the reserved property '${prop}'`; + const testName = `A subclass of Model cannot use the reserved property '${prop}'`; testInDebug(testName, async function (assert) { const NativePost = PROP_MAP[prop]; @@ -669,7 +668,7 @@ module('unit/model - Model', function (hooks) { test('ensure model exits loading state, materializes data and fulfills promise only after data is available', async function (assert) { assert.expect(2); adapter.findRecord = () => - resolve({ + Promise.resolve({ data: { id: '1', type: 'person', @@ -677,14 +676,14 @@ module('unit/model - Model', function (hooks) { }, }); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.strictEqual(get(person, 'currentState.stateName'), 'root.loaded.saved', 'model is in loaded state'); assert.true(get(person, 'isLoaded'), 'model is loaded'); }); test('Pushing a record into the store should transition new records to the loaded state', async function (assert) { - let person = store.createRecord('person', { id: '1', name: 'TomHuda' }); + const person = store.createRecord('person', { id: '1', name: 'TomHuda' }); assert.true(person.isNew, 'createRecord should put records into the new state'); @@ -754,7 +753,7 @@ module('unit/model - Model', function (hooks) { test('a Model can update its attributes', async function (assert) { assert.expect(1); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '2', @@ -776,7 +775,7 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:tag', Tag); - let tag = store.createRecord('tag'); + const tag = store.createRecord('tag'); assert.strictEqual(get(tag, 'name'), 'unknown', 'the default value is found'); set(tag, 'name', null); @@ -806,7 +805,7 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:native-tag', NativeTag); this.owner.register('model:legacy-tag', LegacyTag); - let legacyTag = store.createRecord('legacy-tag', { name: 'old' }); + const legacyTag = store.createRecord('legacy-tag', { name: 'old' }); assert.strictEqual(get(legacyTag, 'name'), 'old', 'precond - name is correct'); set(legacyTag, 'name', 'edited'); @@ -815,7 +814,7 @@ module('unit/model - Model', function (hooks) { set(legacyTag, 'title', 'new'); assert.strictEqual(get(legacyTag, 'name'), 'new', 'setUnknownProperty was triggered'); - let nativeTag = store.createRecord('native-tag', { name: 'old' }); + const nativeTag = store.createRecord('native-tag', { name: 'old' }); assert.strictEqual(get(nativeTag, 'name'), 'old', 'precond - name is correct'); set(nativeTag, 'name', 'edited'); @@ -848,7 +847,7 @@ module('unit/model - Model', function (hooks) { }); test('setting a property back to its original value cleans the mutated state', async function (assert) { - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -875,7 +874,7 @@ module('unit/model - Model', function (hooks) { module('Mutation', function () { test('can have properties and non-specified properties set on it', async function (assert) { - let record = store.createRecord('person', { isDrugAddict: false, notAnAttr: 'my value' }); + const record = store.createRecord('person', { isDrugAddict: false, notAnAttr: 'my value' }); set(record, 'name', 'bar'); set(record, 'anotherNotAnAttr', 'my other value'); @@ -897,7 +896,7 @@ module('unit/model - Model', function (hooks) { }, }); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.false(person.hasDirtyAttributes, 'precond - person record should not be dirty'); @@ -919,7 +918,7 @@ module('unit/model - Model', function (hooks) { }, }); - let person = await store.findRecord('person', '1'); + const person = await store.findRecord('person', '1'); assert.false(person.hasDirtyAttributes, 'precond - person record should not be dirty'); @@ -934,7 +933,7 @@ module('unit/model - Model', function (hooks) { test('resetting a property to the current in-flight value causes it to become clean when the save completes', async function (assert) { adapter.updateRecord = function () { - return resolve(); + return Promise.resolve(); }; store.push({ @@ -947,10 +946,10 @@ module('unit/model - Model', function (hooks) { }, }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); person.set('name', 'Thomas'); - let saving = person.save(); + const saving = person.save(); assert.strictEqual(person.name, 'Thomas'); @@ -977,7 +976,7 @@ module('unit/model - Model', function (hooks) { }, }); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); assert.false(person.hasDirtyAttributes, 'precond - person record should not be dirty'); person.set('isDrugAddict', false); @@ -992,7 +991,7 @@ module('unit/model - Model', function (hooks) { test('an invalid record becomes clean again if changed property is reset', async function (assert) { adapter.updateRecord = () => { - return reject( + return Promise.reject( new InvalidError([ { source: { @@ -1014,7 +1013,7 @@ module('unit/model - Model', function (hooks) { }, }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.false(person.hasDirtyAttributes, 'precond - person record should not be dirty'); person.set('name', 'Wolf'); @@ -1040,7 +1039,7 @@ module('unit/model - Model', function (hooks) { test('an invalid record stays dirty if only invalid property is reset', async function (assert) { adapter.updateRecord = () => { - return reject( + return Promise.reject( new InvalidError([ { source: { @@ -1062,7 +1061,7 @@ module('unit/model - Model', function (hooks) { }, }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.false(person.hasDirtyAttributes, 'precond - person record should not be dirty'); person.set('name', 'Wolf'); @@ -1094,8 +1093,8 @@ module('unit/model - Model', function (hooks) { } this.owner.register('model:post', Post); - let dateString = 'Sat, 31 Dec 2011 00:08:16 GMT'; - let date = new Date(dateString); + const dateString = 'Sat, 31 Dec 2011 00:08:16 GMT'; + const date = new Date(dateString); store.push({ data: { @@ -1104,7 +1103,7 @@ module('unit/model - Model', function (hooks) { }, }); - let record = await store.findRecord('post', '1'); + const record = await store.findRecord('post', '1'); record.set('updatedAt', date); @@ -1128,7 +1127,7 @@ module('unit/model - Model', function (hooks) { this.owner.register('model:mascot', Mascot); - let mascot = store.push({ + const mascot = store.push({ data: { type: 'mascot', id: '1', @@ -1145,7 +1144,7 @@ module('unit/model - Model', function (hooks) { mascot.set('likes', 'Ember.js'); // changed value mascot.set('isMascot', true); // same value - let changedAttributes = mascot.changedAttributes(); + const changedAttributes = mascot.changedAttributes(); assert.deepEqual(changedAttributes.name, [undefined, 'Tomster']); assert.deepEqual(changedAttributes.likes, ['JavaScript', 'Ember.js']); @@ -1177,12 +1176,10 @@ module('unit/model - Model', function (hooks) { likes: [undefined, 'Cheese'], }); - return resolve({ data: { id: '1', type: 'mascot' } }); + return Promise.resolve({ data: { id: '1', type: 'mascot' } }); }; - let cat; - - cat = store.createRecord('mascot'); + const cat = store.createRecord('mascot'); cat.setProperties({ name: 'Argon', likes: 'Cheese', @@ -1193,6 +1190,7 @@ module('unit/model - Model', function (hooks) { test('changedAttributes() works while the record is being updated', async function (assert) { assert.expect(1); + // eslint-disable-next-line prefer-const let cat; class Mascot extends Model { @@ -1235,7 +1233,7 @@ module('unit/model - Model', function (hooks) { test('changedAttributes() reset after save', async function (assert) { adapter.updateRecord = function (store, type, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'person', @@ -1275,6 +1273,7 @@ module('unit/model - Model', function (hooks) { test('@attr decorator works without parens', async function (assert) { assert.expect(1); + // eslint-disable-next-line prefer-const let cat; class Mascot extends Model { @@ -1310,11 +1309,11 @@ module('unit/model - Model', function (hooks) { module('Misc', function () { testInDebug('Calling record.attr() asserts', async function (assert) { - let person = store.createRecord('person', { id: '1', name: 'TomHuda' }); + const person = store.createRecord('person', { id: '1', name: 'TomHuda' }); assert.expectAssertion(() => { person.attr(); - }, /Assertion Failed: The `attr` method is not available on Model, a Snapshot was probably expected\. Are you passing a Model instead of a Snapshot to your serializer\?/); + }, /The `attr` method is not available on Model, a Snapshot was probably expected\. Are you passing a Model instead of a Snapshot to your serializer\?/); }); }); }); diff --git a/tests/main/tests/unit/model/attr-test.js b/tests/main/tests/unit/model/attr-test.js index 428f6b5be49..893379aafee 100644 --- a/tests/main/tests/unit/model/attr-test.js +++ b/tests/main/tests/unit/model/attr-test.js @@ -24,14 +24,14 @@ module('unit/model/attr | attr syntax', function (hooks) { owner.register('model:user', User); - let UserModel = store.modelFor('user'); - let attrs = UserModel.attributes; + const UserModel = store.modelFor('user'); + const attrs = UserModel.attributes; assert.true(attrs.has('name'), 'We have the attr: name'); assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); - let userRecord = store.push({ + const userRecord = store.push({ data: { type: 'user', id: '1', @@ -64,14 +64,14 @@ module('unit/model/attr | attr syntax', function (hooks) { owner.register('model:user', User); - let UserModel = store.modelFor('user'); - let attrs = UserModel.attributes; + const UserModel = store.modelFor('user'); + const attrs = UserModel.attributes; assert.true(attrs.has('name'), 'We have the attr: name'); assert.true(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); assert.true(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); assert.true(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); - let userRecord = store.push({ + const userRecord = store.push({ data: { type: 'user', id: '1', @@ -105,14 +105,14 @@ module('unit/model/attr | attr syntax', function (hooks) { owner.register('model:user', User); - let UserModel = store.modelFor('user'); - let attrs = UserModel.attributes; + const UserModel = store.modelFor('user'); + const attrs = UserModel.attributes; assert.false(attrs.has('name'), 'We have the attr: name'); assert.false(attrs.has('nameWithTransform'), 'We have the attr: nameWithTransform'); assert.false(attrs.has('nameWithOptions'), 'We have the attr: nameWithOptions'); assert.false(attrs.has('nameWithTransformAndOptions'), 'We have the attr: nameWithTransformAndOptions'); - let userRecord = store.push({ + const userRecord = store.push({ data: { type: 'user', id: '1', @@ -142,11 +142,11 @@ module('unit/model/attr | attr syntax', function (hooks) { owner.register('model:user', User); - let UserModel = store.modelFor('user'); - let attrs = UserModel.attributes; + const UserModel = store.modelFor('user'); + const attrs = UserModel.attributes; assert.true(attrs.has('name'), 'We have the attr: name'); - let userRecord = store.push({ + const userRecord = store.push({ data: { type: 'user', id: '1', @@ -166,11 +166,11 @@ module('unit/model/attr | attr syntax', function (hooks) { owner.register('model:blog', Blog); - let BlogModel = store.modelFor('blog'); - let attrs = BlogModel.attributes; + const BlogModel = store.modelFor('blog'); + const attrs = BlogModel.attributes; assert.true(attrs.has('content'), 'We have the attr: name'); - let userRecord = store.push({ + const userRecord = store.push({ data: { type: 'blog', id: '1', diff --git a/tests/main/tests/unit/model/init-properties-test.js b/tests/main/tests/unit/model/init-properties-test.js index 02a1d7fcde0..433e6aead41 100644 --- a/tests/main/tests/unit/model/init-properties-test.js +++ b/tests/main/tests/unit/model/init-properties-test.js @@ -1,9 +1,7 @@ import { get } from '@ember/object'; -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -12,8 +10,6 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; function setupModels(owner, testState) { - let types; - const Comment = Model.extend({ text: attr(), post: belongsTo('post', { async: false, inverse: 'comments' }), @@ -34,7 +30,7 @@ function setupModels(owner, testState) { }, }); - types = { + const types = { Author, Comment, Post, @@ -47,8 +43,8 @@ function setupModels(owner, testState) { owner.register('adapter:application', JSONAPIAdapter.extend()); owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = owner.lookup('service:store'); + const adapter = store.adapterFor('application'); return { adapter, store }; } @@ -58,8 +54,6 @@ module('unit/model - init properties', function (hooks) { test('createRecord(properties) makes properties available during record init', function (assert) { assert.expect(4); - let comment; - let author; function testState(types, record) { assert.strictEqual(get(record, 'title'), 'My Post', 'Attrs are available as expected'); @@ -68,9 +62,9 @@ module('unit/model - init properties', function (hooks) { assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } - let { store } = setupModels(this.owner, testState); + const { store } = setupModels(this.owner, testState); - comment = store.push({ + const comment = store.push({ data: { type: 'comment', id: '1', @@ -79,7 +73,7 @@ module('unit/model - init properties', function (hooks) { }, }, }); - author = store.push({ + const author = store.push({ data: { type: 'author', id: '1', @@ -106,7 +100,7 @@ module('unit/model - init properties', function (hooks) { assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } - let { store } = setupModels(this.owner, testState); + const { store } = setupModels(this.owner, testState); store.push({ data: { @@ -154,10 +148,10 @@ module('unit/model - init properties', function (hooks) { assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } - let { adapter, store } = setupModels(this.owner, testState); + const { adapter, store } = setupModels(this.owner, testState); adapter.findRecord = () => { - return resolve({ + return Promise.resolve({ data: { type: 'post', id: '1', @@ -204,10 +198,10 @@ module('unit/model - init properties', function (hooks) { assert.ok(record.comments.at(0) instanceof types.Comment, 'hasMany relationships are available as expected'); } - let { adapter, store } = setupModels(this.owner, testState); + const { adapter, store } = setupModels(this.owner, testState); adapter.queryRecord = () => { - return resolve({ + return Promise.resolve({ data: { type: 'post', id: '1', @@ -245,7 +239,7 @@ module('unit/model - init properties', function (hooks) { await store.queryRecord('post', { id: '1' }); }); - test('Model class does not get properties passed to setUknownProperty accidentally', function (assert) { + test('Model class does not get properties passed to setUnknownProperty accidentally', function (assert) { assert.expect(2); // If we end up passing additional properties to init in modelClasses, we will need to come up with a strategy for // how to get setUnknownProperty to continue working @@ -253,8 +247,8 @@ module('unit/model - init properties', function (hooks) { const Post = Model.extend({ title: attr(), setUnknownProperty: function (key, value) { - assert.strictEqual(key, 'randomProp', 'Passed the correct key to setUknownProperty'); - assert.strictEqual(value, 'An unknown prop', 'Passed the correct value to setUknownProperty'); + assert.strictEqual(key, 'randomProp', 'Passed the correct key to setUnknownProperty'); + assert.strictEqual(value, 'An unknown prop', 'Passed the correct value to setUnknownProperty'); }, }); @@ -262,13 +256,11 @@ module('unit/model - init properties', function (hooks) { this.owner.register('adapter:application', JSONAPIAdapter.extend()); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(() => { - store.createRecord('post', { - title: 'My Post', - randomProp: 'An unknown prop', - }); + store.createRecord('post', { + title: 'My Post', + randomProp: 'An unknown prop', }); }); }); diff --git a/tests/main/tests/unit/model/merge-test.js b/tests/main/tests/unit/model/merge-test.js index b671b26502a..951cfcddde8 100644 --- a/tests/main/tests/unit/model/merge-test.js +++ b/tests/main/tests/unit/model/merge-test.js @@ -1,7 +1,4 @@ -import { next, run } from '@ember/runloop'; - import { module, test } from 'qunit'; -import { Promise as EmberPromise, reject, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -25,7 +22,7 @@ module('unit/model/merge - Merging', function (hooks) { this.store = this.owner.lookup('service:store'); }); - test('When a record is in flight, changes can be made', function (assert) { + test('When a record is in flight, changes can be made', async function (assert) { assert.expect(3); const ApplicationAdapter = Adapter.extend({ @@ -36,162 +33,143 @@ module('unit/model/merge - Merging', function (hooks) { this.owner.register('adapter:application', ApplicationAdapter); - let person = this.store.createRecord('person', { name: 'Tom Dale' }); - - // Make sure saving isn't resolved synchronously - return run(() => { - let save = person.save(); + const person = this.store.createRecord('person', { name: 'Tom Dale' }); + const save = person.save(); - assert.strictEqual(person.name, 'Tom Dale'); + assert.strictEqual(person.name, 'Tom Dale'); - person.set('name', 'Thomas Dale'); + person.set('name', 'Thomas Dale'); - return save.then((person) => { - assert.true(person.hasDirtyAttributes, 'The person is still dirty'); - assert.strictEqual(person.name, 'Thomas Dale', 'The changes made still apply'); - }); + await save.then((person) => { + assert.true(person.hasDirtyAttributes, 'The person is still dirty'); + assert.strictEqual(person.name, 'Thomas Dale', 'The changes made still apply'); }); }); - test('Make sure snapshot is created at save time not at flush time', function (assert) { + test('Make sure snapshot is created at save time not at flush time', async function (assert) { assert.expect(5); const ApplicationAdapter = Adapter.extend({ updateRecord(store, type, snapshot) { assert.strictEqual(snapshot.attr('name'), 'Thomas Dale'); - return resolve(); + return Promise.resolve(); }, }); this.owner.register('adapter:application', ApplicationAdapter); - let person; - run(() => { - person = this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', }, - }); - person.set('name', 'Thomas Dale'); + }, }); + person.set('name', 'Thomas Dale'); - return run(() => { - let promise = person.save(); + const promise = person.save(); - assert.strictEqual(person.name, 'Thomas Dale'); + assert.strictEqual(person.name, 'Thomas Dale'); - person.set('name', 'Tomasz Dale'); + person.set('name', 'Tomasz Dale'); - assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes applied on top'); + assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes applied on top'); - return promise.then((person) => { - assert.true(person.hasDirtyAttributes, 'The person is still dirty'); - assert.strictEqual(person.name, 'Tomasz Dale', 'The local changes apply'); - }); + await promise.then((person) => { + assert.true(person.hasDirtyAttributes, 'The person is still dirty'); + assert.strictEqual(person.name, 'Tomasz Dale', 'The local changes apply'); }); }); - test('When a record is in flight, pushes are applied underneath the in flight changes', function (assert) { + test('When a record is in flight, pushes are applied underneath the in flight changes', async function (assert) { assert.expect(6); const ApplicationAdapter = Adapter.extend({ updateRecord(store, type, snapshot) { // Make sure saving isn't resolved synchronously - return new EmberPromise((resolve) => { - next(null, resolve, { - data: { - id: '1', - type: 'person', - attributes: { name: 'Senor Thomas Dale, Esq.', city: 'Portland' }, - }, - }); + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + data: { + id: '1', + type: 'person', + attributes: { name: 'Senor Thomas Dale, Esq.', city: 'Portland' }, + }, + }); + }, 0); }); }, }); this.owner.register('adapter:application', ApplicationAdapter); - let person; - - run(() => { - person = this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom', }, - }); - person.set('name', 'Thomas Dale'); + }, }); + person.set('name', 'Thomas Dale'); - return run(() => { - var promise = person.save(); + const promise = person.save(); - assert.strictEqual(person.name, 'Thomas Dale'); + assert.strictEqual(person.name, 'Thomas Dale'); - person.set('name', 'Tomasz Dale'); + person.set('name', 'Tomasz Dale'); - this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tommy Dale', - city: 'PDX', - }, + this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tommy Dale', + city: 'PDX', }, - }); + }, + }); - assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes applied on top'); - assert.strictEqual(person.city, 'PDX', 'the pushed change is available'); + assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes applied on top'); + assert.strictEqual(person.city, 'PDX', 'the pushed change is available'); - return promise.then((person) => { - assert.true(person.hasDirtyAttributes, 'The person is still dirty'); - assert.strictEqual(person.name, 'Tomasz Dale', 'The local changes apply'); - assert.strictEqual(person.city, 'Portland', 'The updates from the server apply on top of the previous pushes'); - }); + await promise.then((person) => { + assert.true(person.hasDirtyAttributes, 'The person is still dirty'); + assert.strictEqual(person.name, 'Tomasz Dale', 'The local changes apply'); + assert.strictEqual(person.city, 'Portland', 'The updates from the server apply on top of the previous pushes'); }); }); test('When a record is dirty, pushes are overridden by local changes', function (assert) { - let person; - - run(() => { - person = this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - city: 'San Francisco', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + city: 'San Francisco', }, - }); - person.set('name', 'Tomasz Dale'); + }, }); + person.set('name', 'Tomasz Dale'); assert.true(person.hasDirtyAttributes, 'the person is currently dirty'); assert.strictEqual(person.name, 'Tomasz Dale', 'the update was effective'); assert.strictEqual(person.city, 'San Francisco', 'the original data applies'); - run(() => { - this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Thomas Dale', - city: 'Portland', - }, + this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Thomas Dale', + city: 'Portland', }, - }); + }, }); assert.true(person.hasDirtyAttributes, 'the local changes are reapplied'); @@ -202,25 +180,21 @@ module('unit/model/merge - Merging', function (hooks) { test('When a record is invalid, pushes are overridden by local changes', async function (assert) { const ApplicationAdapter = Adapter.extend({ updateRecord() { - return reject(new InvalidError()); + return Promise.reject(new InvalidError()); }, }); this.owner.register('adapter:application', ApplicationAdapter); - let person; - - run(() => { - person = this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Brendan McLoughlin', - city: 'Boston', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Brendan McLoughlin', + city: 'Boston', }, - }); + }, }); person.set('name', 'Brondan McLoughlin'); @@ -228,7 +202,7 @@ module('unit/model/merge - Merging', function (hooks) { try { await person.save(); assert.ok(false, 'We should throw during save'); - } catch (e) { + } catch { assert.ok(true, 'We rejected the save'); } assert.false(person.isValid, 'the person is currently invalid'); @@ -236,17 +210,15 @@ module('unit/model/merge - Merging', function (hooks) { assert.strictEqual(person.name, 'Brondan McLoughlin', 'the update was effective'); assert.strictEqual(person.city, 'Boston', 'the original data applies'); - run(() => { - this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'bmac', - city: 'Prague', - }, + this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'bmac', + city: 'Prague', }, - }); + }, }); assert.true(person.hasDirtyAttributes, 'the local changes are reapplied'); @@ -255,7 +227,7 @@ module('unit/model/merge - Merging', function (hooks) { assert.strictEqual(person.city, 'Prague', 'if there are no local changes, the new data applied'); }); - test('A record with no changes can still be saved', function (assert) { + test('A record with no changes can still be saved', async function (assert) { assert.expect(1); const ApplicationAdapter = Adapter.extend({ @@ -266,26 +238,21 @@ module('unit/model/merge - Merging', function (hooks) { this.owner.register('adapter:application', ApplicationAdapter); - let person = run(() => { - return this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', }, - }); + }, }); - return run(() => { - return person.save().then((foo) => { - assert.strictEqual(person.name, 'Thomas Dale', 'the updates occurred'); - }); - }); + await person.save(); + assert.strictEqual(person.name, 'Thomas Dale', 'the updates occurred'); }); - test('A dirty record can be reloaded', function (assert) { + test('A dirty record can be reloaded', async function (assert) { assert.expect(3); const ApplicationAdapter = Adapter.extend({ @@ -298,27 +265,21 @@ module('unit/model/merge - Merging', function (hooks) { this.owner.register('adapter:application', ApplicationAdapter); - let person; - - run(() => { - person = this.store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, + const person = this.store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', }, - }); - person.set('name', 'Tomasz Dale'); + }, }); + person.set('name', 'Tomasz Dale'); - return run(() => { - return person.reload().then(() => { - assert.true(person.hasDirtyAttributes, 'the person is dirty'); - assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes remain'); - assert.strictEqual(person.city, 'Portland', 'the new changes apply'); - }); + await person.reload().then(() => { + assert.true(person.hasDirtyAttributes, 'the person is dirty'); + assert.strictEqual(person.name, 'Tomasz Dale', 'the local changes remain'); + assert.strictEqual(person.city, 'Portland', 'the new changes apply'); }); }); }); diff --git a/tests/main/tests/unit/model/relationships-test.js b/tests/main/tests/unit/model/relationships-test.js index 3a49ce88925..6cceea0bde9 100644 --- a/tests/main/tests/unit/model/relationships-test.js +++ b/tests/main/tests/unit/model/relationships-test.js @@ -28,7 +28,7 @@ module('[@ember-data/model] unit - relationships', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; + const { owner } = this; owner.register('model:occupation', Occupation); owner.register('model:person', Person); @@ -39,11 +39,11 @@ module('[@ember-data/model] unit - relationships', function (hooks) { }); test('exposes a hash of the relationships on a model', function (assert) { - let Person = store.modelFor('person'); + const Person = store.modelFor('person'); - let relationships = get(Person, 'relationships'); + const relationships = get(Person, 'relationships'); function extractDetails(key) { - let descs = relationships.get(key); + const descs = relationships.get(key); return descs.map((desc) => { return { @@ -81,7 +81,7 @@ module('[@ember-data/model] unit - relationships', function (hooks) { }); test('eachRelatedType() iterates over relations without duplication', function (assert) { - let relations = []; + const relations = []; Person.eachRelatedType((modelName) => relations.push(modelName)); @@ -89,7 +89,7 @@ module('[@ember-data/model] unit - relationships', function (hooks) { }); test('normalizing belongsTo relationship names', function (assert) { - let User = store.modelFor('user'); + const User = store.modelFor('user'); const relationships = get(User, 'relationships'); @@ -101,8 +101,7 @@ module('[@ember-data/model] unit - relationships', function (hooks) { }); test('normalizing hasMany relationship names', function (assert) { - let store; - let { owner } = this; + const { owner } = this; class StreamItem extends Model { @belongsTo('user', { async: true, inverse: 'streamItems' }) user; @@ -116,9 +115,9 @@ module('[@ember-data/model] unit - relationships', function (hooks) { owner.register('model:stream-item', StreamItem); owner.register('model:user', User); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store'); - let user = store.modelFor('user'); + const user = store.modelFor('user'); const relationships = get(user, 'relationships'); @@ -133,8 +132,7 @@ module('[@ember-data/model] unit - relationships', function (hooks) { 'decorators works without parens', { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 6 }, function (assert) { - let store; - let { owner } = this; + const { owner } = this; class StreamItem extends Model { @belongsTo user; @@ -148,9 +146,9 @@ module('[@ember-data/model] unit - relationships', function (hooks) { owner.register('model:stream-item', StreamItem); owner.register('model:user', User); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store'); - let user = store.modelFor('user'); + const user = store.modelFor('user'); const relationships = get(user, 'relationships'); diff --git a/tests/main/tests/unit/model/relationships/belongs-to-test.js b/tests/main/tests/unit/model/relationships/belongs-to-test.js index 2e4f6d94ac5..d10e8fb30fa 100644 --- a/tests/main/tests/unit/model/relationships/belongs-to-test.js +++ b/tests/main/tests/unit/model/relationships/belongs-to-test.js @@ -1,14 +1,13 @@ import { get } from '@ember/object'; -import { run } from '@ember/runloop'; import { module, test } from 'qunit'; -import { Promise } from 'rsvp'; 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'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('unit/model/relationships - belongsTo', function (hooks) { @@ -35,8 +34,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -109,55 +108,51 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - run(() => { - store.push({ - data: [ - { - type: 'tag', - id: '1', - attributes: { - name: 'whatever', - }, + store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'whatever', }, - { - type: 'person', - id: '2', - attributes: { - name: 'David J. Hamilton', - }, - relationships: { - tag: { - data: { - type: 'tag', - id: '1', - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'David J. Hamilton', + }, + relationships: { + tag: { + data: { + type: 'tag', + id: '1', }, }, }, - ], - }); + }, + ], }); - return run(() => { - let person = store.peekRecord('person', 2); + const person = store.peekRecord('person', '2'); - let tagDidChange = () => assert.ok(false, 'observer is not called'); + const tagDidChange = () => assert.ok(false, 'observer is not called'); - person.addObserver('tag', tagDidChange); + person.addObserver('tag', tagDidChange); - assert.strictEqual(person.tag.name, 'whatever', 'relationship is correct'); + assert.strictEqual(person.tag.name, 'whatever', 'relationship is correct'); - // This needs to be removed so it is not triggered when test context is torn down - person.removeObserver('tag', tagDidChange); - }); + // This needs to be removed so it is not triggered when test context is torn down + person.removeObserver('tag', tagDidChange); }); - test('async belongsTo relationships work when the data hash has not been loaded', function (assert) { + test('async belongsTo relationships work when the data hash has not been loaded', async function (assert) { assert.expect(5); const Tag = Model.extend({ @@ -172,8 +167,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { if (type === Person) { @@ -194,21 +189,17 @@ module('unit/model/relationships - belongsTo', function (hooks) { } }; - return run(() => { - return store - .findRecord('person', 1) - .then((person) => { - assert.strictEqual(get(person, 'name'), 'Tom Dale', 'The person is now populated'); - - return run(() => { - return get(person, 'tag'); - }); - }) - .then((tag) => { - assert.strictEqual(get(tag, 'name'), 'friendly', 'Tom Dale is now friendly'); - assert.true(get(tag, 'isLoaded'), 'Tom Dale is now loaded'); - }); - }); + await store + .findRecord('person', '1') + .then((person) => { + assert.strictEqual(get(person, 'name'), 'Tom Dale', 'The person is now populated'); + + return person.tag; + }) + .then((tag) => { + assert.strictEqual(get(tag, 'name'), 'friendly', 'Tom Dale is now friendly'); + assert.true(get(tag, 'isLoaded'), 'Tom Dale is now loaded'); + }); }); test('async belongsTo relationships are not grouped with coalesceFindRequests=false', async function (assert) { @@ -226,8 +217,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.coalesceFindRequests = false; @@ -286,8 +277,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { } }; - let persons = [store.peekRecord('person', '1'), store.peekRecord('person', '2')]; - let [tag1, tag2] = await Promise.all(persons.map((person) => get(person, 'tag'))); + const persons = [store.peekRecord('person', '1'), store.peekRecord('person', '2')]; + const [tag1, tag2] = await Promise.all(persons.map((person) => person.tag)); assert.strictEqual(get(tag1, 'name'), 'friendly', 'Tom Dale is now friendly'); assert.true(get(tag1, 'isLoaded'), "Tom Dale's tag is now loaded"); @@ -311,8 +302,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.coalesceFindRequests = true; @@ -369,8 +360,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { throw new Error('findRecord should not be called'); }; - let persons = [store.peekRecord('person', '1'), store.peekRecord('person', '2')]; - let [tag1, tag2] = await Promise.all(persons.map((person) => get(person, 'tag'))); + const persons = [store.peekRecord('person', '1'), store.peekRecord('person', '2')]; + const [tag1, tag2] = await Promise.all(persons.map((person) => person.tag)); assert.strictEqual(get(tag1, 'name'), 'friendly', 'Tom Dale is now friendly'); assert.true(get(tag1, 'isLoaded'), "Tom Dale's tag is now loaded"); @@ -379,7 +370,7 @@ module('unit/model/relationships - belongsTo', function (hooks) { assert.true(get(tag2, 'isLoaded'), "Bob Dylan's tag is now loaded"); }); - test('async belongsTo relationships work when the data hash has already been loaded', function (assert) { + test('async belongsTo relationships work when the data hash has already been loaded', async function (assert) { assert.expect(3); const Tag = Model.extend({ @@ -394,104 +385,105 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: [ - { - type: 'tag', - id: '2', - attributes: { - name: 'friendly', - }, + store.push({ + data: [ + { + type: 'tag', + id: '2', + attributes: { + name: 'friendly', }, - { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - tag: { - data: { type: 'tag', id: '2' }, - }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + tag: { + data: { type: 'tag', id: '2' }, }, }, - ], - }); + }, + ], }); - return run(() => { - let person = store.peekRecord('person', 1); - assert.strictEqual(get(person, 'name'), 'Tom Dale', 'The person is now populated'); - return run(() => { - return get(person, 'tag'); - }).then((tag) => { - assert.strictEqual(get(tag, 'name'), 'friendly', 'Tom Dale is now friendly'); - assert.true(get(tag, 'isLoaded'), 'Tom Dale is now loaded'); - }); - }); + const person = store.peekRecord('person', 1); + assert.strictEqual(get(person, 'name'), 'Tom Dale', 'The person is now populated'); + const tag = await person.tag; + assert.strictEqual(get(tag, 'name'), 'friendly', 'Tom Dale is now friendly'); + assert.true(get(tag, 'isLoaded'), 'Tom Dale is now loaded'); }); - test('when response to saving a belongsTo is a success but includes changes that reset the users change', async function (assert) { - const Tag = Model.extend({ - label: attr(), - }); - const User = Model.extend({ - tag: belongsTo('tag', { async: false, inverse: null }), - }); - - this.owner.register('model:tag', Tag); - this.owner.register('model:user', User); + deprecatedTest( + 'when response to saving a belongsTo is a success but includes changes that reset the users change', + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + until: '6.0', + count: 1, + refactor: true, // we assert against this scenario in dev at the cache level by comparing to in-flight state + }, + async function (assert) { + class Tag extends Model { + @attr label; + } + class User extends Model { + @belongsTo('tag', { async: false, inverse: null }) tag; + } - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + this.owner.register('model:tag', Tag); + this.owner.register('model:user', User); - const [user, tag1, tag2] = store.push({ - data: [ - { - type: 'user', - id: '1', - relationships: { - tag: { - data: { type: 'tag', id: '1' }, + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + const [user, tag1, tag2] = store.push({ + data: [ + { + type: 'user', + id: '1', + relationships: { + tag: { + data: { type: 'tag', id: '1' }, + }, }, }, - }, - { type: 'tag', id: '1', attributes: { label: 'A' } }, - { type: 'tag', id: '2', attributes: { label: 'B' } }, - ], - }); + { type: 'tag', id: '1', attributes: { label: 'A' } }, + { type: 'tag', id: '2', attributes: { label: 'B' } }, + ], + }); - assert.strictEqual(tag1.label, 'A', 'tag1 is loaded'); - assert.strictEqual(tag2.label, 'B', 'tag2 is loaded'); - assert.strictEqual(user.tag.id, '1', 'user starts with tag1 as tag'); + assert.strictEqual(tag1.label, 'A', 'tag1 is loaded'); + assert.strictEqual(tag2.label, 'B', 'tag2 is loaded'); + assert.strictEqual(user.tag.id, '1', 'user starts with tag1 as tag'); - user.set('tag', tag2); + user.set('tag', tag2); - assert.strictEqual(user.tag.id, '2', 'user tag updated to tag2'); + assert.strictEqual(user.tag.id, '2', 'user tag updated to tag2'); - adapter.updateRecord = function () { - return { - data: { - type: 'user', - id: '1', - relationships: { - tag: { - data: { - id: '1', - type: 'tag', + adapter.updateRecord = function () { + return { + data: { + type: 'user', + id: '1', + relationships: { + tag: { + data: { + id: '1', + type: 'tag', + }, }, }, }, - }, + }; }; - }; - await user.save(); - assert.strictEqual(user.tag.id, '1', 'expected new server state to be applied'); - }); + await user.save(); + assert.strictEqual(user.tag.id, '1', 'expected new server state to be applied'); + } + ); test('calling createRecord and passing in an undefined value for a relationship should be treated as if null', function (assert) { assert.expect(1); @@ -509,21 +501,16 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - store.createRecord('person', { id: '1', tag: undefined }); - - return run(() => { - return store.findRecord('person', '1').then((person) => { - assert.strictEqual(person.tag, null, 'undefined values should return null relationships'); - }); - }); + const person = store.createRecord('person', { tag: undefined }); + assert.strictEqual(person.tag, null, 'undefined values should return null relationships'); }); - test('When finding a hasMany relationship the inverse belongsTo relationship is available immediately', function (assert) { + test('When finding a hasMany relationship the inverse belongsTo relationship is available immediately', async function (assert) { const Occupation = Model.extend({ description: attr('string'), person: belongsTo('person', { async: false, inverse: 'occupations' }), @@ -537,8 +524,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:occupation', Occupation); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -554,42 +541,38 @@ module('unit/model/relationships - belongsTo', function (hooks) { adapter.coalesceFindRequests = true; - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - occupations: { - data: [ - { type: 'occupation', id: '5' }, - { type: 'occupation', id: '2' }, - ], - }, + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + occupations: { + data: [ + { type: 'occupation', id: '5' }, + { type: 'occupation', id: '2' }, + ], }, }, - }); + }, }); - return run(() => { - return store - .findRecord('person', 1) - .then((person) => { - assert.true(get(person, 'isLoaded'), 'isLoaded should be true'); - assert.strictEqual(get(person, 'name'), 'Tom Dale', 'the person is still Tom Dale'); + await store + .findRecord('person', '1') + .then((person) => { + assert.true(get(person, 'isLoaded'), 'isLoaded should be true'); + assert.strictEqual(get(person, 'name'), 'Tom Dale', 'the person is still Tom Dale'); - return get(person, 'occupations'); - }) - .then((occupations) => { - assert.strictEqual(get(occupations, 'length'), 2, 'the list of occupations should have the correct length'); + return person.occupations; + }) + .then((occupations) => { + assert.strictEqual(get(occupations, 'length'), 2, 'the list of occupations should have the correct length'); - assert.strictEqual(get(occupations.at(0), 'description'), 'fifth', 'the occupation is the fifth'); - assert.true(get(occupations.at(0), 'isLoaded'), 'the occupation is now loaded'); - }); - }); + assert.strictEqual(get(occupations.at(0), 'description'), 'fifth', 'the occupation is the fifth'); + assert.true(get(occupations.at(0), 'isLoaded'), 'the occupation is now loaded'); + }); }); test('When finding a belongsTo relationship the inverse belongsTo relationship is available immediately', async function (assert) { @@ -608,8 +591,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:occupation', Occupation); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { assert.strictEqual(snapshot.belongsTo('person').id, '1'); @@ -650,8 +633,8 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -707,7 +690,7 @@ module('unit/model/relationships - belongsTo', function (hooks) { ); }); - testInDebug('belongsTo gives a warning when provided with a serialize option', function (assert) { + testInDebug('belongsTo gives a warning when provided with a serialize option', async function (assert) { const Hobby = Model.extend({ name: attr('string'), }); @@ -720,54 +703,50 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:hobby', Hobby); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - run(() => { - store.push({ - data: [ - { - type: 'hobby', - id: '1', - attributes: { - name: 'fishing', - }, + store.push({ + data: [ + { + type: 'hobby', + id: '1', + attributes: { + name: 'fishing', }, - { - type: 'hobby', - id: '2', - attributes: { - name: 'coding', - }, + }, + { + type: 'hobby', + id: '2', + attributes: { + name: 'coding', }, - { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - hobby: { - data: { type: 'hobby', id: '1' }, - }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + hobby: { + data: { type: 'hobby', id: '1' }, }, }, - ], - }); + }, + ], }); - return run(() => { - return store.findRecord('person', 1).then((person) => { - assert.expectWarning(() => { - get(person, 'hobby'); - }, /You provided a serialize option on the "hobby" property in the "person" class, this belongs in the serializer. See Serializer and it's implementations/); - }); + await store.findRecord('person', '1').then((person) => { + assert.expectWarning(() => { + person.hobby; + }, /You provided a serialize option on the "hobby" property in the "person" class, this belongs in the serializer. See Serializer and it's implementations/); }); }); - testInDebug('belongsTo gives a warning when provided with an embedded option', function (assert) { + testInDebug('belongsTo gives a warning when provided with an embedded option', async function (assert) { const Hobby = Model.extend({ name: attr('string'), }); @@ -780,50 +759,46 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:hobby', Hobby); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - run(() => { - store.push({ - data: [ - { - type: 'hobby', - id: '1', - attributes: { - name: 'fishing', - }, + store.push({ + data: [ + { + type: 'hobby', + id: '1', + attributes: { + name: 'fishing', }, - { - type: 'hobby', - id: '2', - attributes: { - name: 'coding', - }, + }, + { + type: 'hobby', + id: '2', + attributes: { + name: 'coding', }, - { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, - relationships: { - hobby: { - data: { type: 'hobby', id: '1' }, - }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + hobby: { + data: { type: 'hobby', id: '1' }, }, }, - ], - }); + }, + ], }); - return run(() => { - return store.findRecord('person', 1).then((person) => { - assert.expectWarning(() => { - get(person, 'hobby'); - }, /You provided an embedded option on the "hobby" property in the "person" class, this belongs in the serializer. See EmbeddedRecordsMixin/); - }); + await store.findRecord('person', '1').then((person) => { + assert.expectWarning(() => { + person.hobby; + }, /You provided an embedded option on the "hobby" property in the "person" class, this belongs in the serializer. See EmbeddedRecordsMixin/); }); }); @@ -841,10 +816,10 @@ module('unit/model/relationships - belongsTo', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - const theTag = store.push({ data: { type: 'tag', id: '1', attributes: { name: 'ember' } } }); - let person = store.createRecord('person', { tag: theTag }); + const theTag = store.push({ data: { type: 'tag', id: '1', attributes: { name: 'Amber' } } }); + const person = store.createRecord('person', { tag: theTag }); const personTag = person.tag; assert.ok(personTag.then, 'tag should be an async relationship'); const tag = await personTag; 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 9f7042f5c96..39444d0ec81 100644 --- a/tests/main/tests/unit/model/relationships/has-many-test.js +++ b/tests/main/tests/unit/model/relationships/has-many-test.js @@ -5,20 +5,14 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; -import { DEPRECATE_ARRAY_LIKE, DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 } from '@ember-data/deprecations'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import { LEGACY_SUPPORT, PromiseManyArray } from '@ember-data/model/-private'; +import { LEGACY_SUPPORT } from '@ember-data/model/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; 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'; - -let IS_DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 = false; - -if (DEPRECATE_MANY_ARRAY_DUPLICATES_4_12) { - IS_DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 = true; -} +import { DEPRECATE_ARRAY_LIKE, DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; module('unit/model/relationships - hasMany', function (hooks) { setupTest(hooks); @@ -191,8 +185,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:pet', Pet); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { if (type === Tag && id === '12') { @@ -271,7 +265,7 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.strictEqual(person.name, 'Tom Dale', 'precond - retrieves person record from store'); - let tags = person.tags; + const tags = person.tags; assert.strictEqual(tags.length, 1, 'the list of tags should have the correct length'); assert.strictEqual(tags.at(0).name, 'friendly', 'the first tag should be a Tag'); @@ -336,7 +330,7 @@ module('unit/model/relationships - hasMany', function (hooks) { assert.strictEqual(cyvid.name, 'Cyvid Hamluck', 'precond - retrieves person record from store'); - let pets = cyvid.pets; + const pets = cyvid.pets; assert.strictEqual(pets.length, 1, 'the list of pets should have the correct length'); assert.strictEqual(pets.at(0).name, 'fluffy', 'the first pet should be correct'); @@ -380,8 +374,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -414,7 +408,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ], }); - let tag = store.peekRecord('tag', 1); + const tag = store.peekRecord('tag', 1); tag.addObserver('people', () => { assert.ok(false, 'observer is not called'); }); @@ -446,8 +440,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -466,7 +460,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let tag = store.peekRecord('tag', 1); + const tag = store.peekRecord('tag', 1); assert.strictEqual(tag.people.length, 0, 'relationship is correct'); }); @@ -487,8 +481,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -546,84 +540,93 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let tag = store.peekRecord('tag', 1); - let person = store.peekRecord('person', 1); + const tag = store.peekRecord('tag', 1); + const person = store.peekRecord('person', 1); assert.strictEqual(person.tag, null, 'relationship is empty'); assert.strictEqual(tag.people.length, 0, 'relationship is correct'); }); - test('hasMany with duplicates from payload', function (assert) { - assert.expect(1); + deprecatedTest( + 'hasMany with duplicates from payload', + { + id: 'ember-data:deprecate-non-unique-relationship-entries', + count: 1, + until: '6.0', + refactor: true, // should assert when stripped + }, + function (assert) { + assert.expect(1); - const Tag = Model.extend({ - name: attr('string'), - people: hasMany('person', { async: false, inverse: 'tag' }), - }); + const Tag = Model.extend({ + name: attr('string'), + people: hasMany('person', { async: false, inverse: 'tag' }), + }); - Tag.toString = () => { - return 'tag'; - }; + Tag.toString = () => { + return 'tag'; + }; - const Person = Model.extend({ - name: attr('string'), - tag: belongsTo('tag', { async: false, inverse: 'people' }), - }); + const Person = Model.extend({ + name: attr('string'), + tag: belongsTo('tag', { async: false, inverse: 'people' }), + }); - Person.toString = () => { - return 'person'; - }; + Person.toString = () => { + return 'person'; + }; - this.owner.register('model:tag', Tag); - this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - // first we push in data with the relationship - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'David J. Hamilton', - }, - relationships: { - tag: { - data: { - type: 'tag', - id: '1', - }, - }, - }, - }, - included: [ - { - type: 'tag', + // first we push in data with the relationship + store.push({ + data: { + type: 'person', id: '1', attributes: { - name: 'whatever', + name: 'David J. Hamilton', }, relationships: { - people: { - data: [ - { - type: 'person', - id: '1', - }, - { - type: 'person', - id: '1', - }, - ], + tag: { + data: { + type: 'tag', + id: '1', + }, }, }, }, - ], - }); + included: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'whatever', + }, + relationships: { + people: { + data: [ + { + type: 'person', + id: '1', + }, + { + type: 'person', + id: '1', + }, + ], + }, + }, + }, + ], + }); - let tag = store.peekRecord('tag', 1); - assert.strictEqual(tag.people.length, 1, 'relationship does not contain duplicates'); - }); + const tag = store.peekRecord('tag', 1); + assert.strictEqual(tag.people.length, 1, 'relationship does not contain duplicates'); + } + ); test('many2many loads both sides #5140', function (assert) { assert.expect(3); @@ -649,7 +652,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); // first we push in data with the relationship store.push({ @@ -745,11 +748,11 @@ module('unit/model/relationships - hasMany', function (hooks) { ], }); - let tag = store.peekRecord('tag', 1); + const tag = store.peekRecord('tag', 1); assert.strictEqual(tag.people.length, 2, 'relationship does contain all data'); - let person1 = store.peekRecord('person', 1); + const person1 = store.peekRecord('person', 1); assert.strictEqual(person1.tags.length, 2, 'relationship does contain all data'); - let person2 = store.peekRecord('person', 2); + const person2 = store.peekRecord('person', 2); assert.strictEqual(person2.tags.length, 2, 'relationship does contain all data'); }); @@ -769,8 +772,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -850,8 +853,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -875,7 +878,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let eddy = store.peekRecord('person', 1); + const eddy = store.peekRecord('person', 1); assert.deepEqual( eddy.trueFriends.map((r) => r.name), ['Edward II'], @@ -906,8 +909,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:pet', Pet); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id, snapshot) { if (type === Tag && id === '12') { @@ -999,7 +1002,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); const tagsAgain = await wycats.tags; - let newTag = store.createRecord('tag'); + const newTag = store.createRecord('tag'); tagsAgain.push(newTag); await settled(); @@ -1017,7 +1020,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.strictEqual( store.modelFor('person').typeForRelationship('tags', store), @@ -1036,7 +1039,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.strictEqual( store.modelFor('person').typeForRelationship('tags', store), @@ -1055,7 +1058,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.strictEqual( store.modelFor('person').typeForRelationship('tag', store), @@ -1076,7 +1079,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.strictEqual( store.modelFor('person').typeForRelationship('tags', store), @@ -1100,8 +1103,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -1168,8 +1171,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.coalesceFindRequests = true; adapter.findMany = function (store, type, ids, snapshots) { @@ -1231,8 +1234,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -1254,7 +1257,7 @@ module('unit/model/relationships - hasMany', function (hooks) { type: 'tag', id: '1', attributes: { - name: 'ember', + name: 'Amber', }, }, ], @@ -1263,7 +1266,7 @@ module('unit/model/relationships - hasMany', function (hooks) { await store.findRecord('person', '1').then((person) => { let tag = person.tags.at(0); - assert.strictEqual(tag.name, 'ember', 'precond - relationships work'); + assert.strictEqual(tag.name, 'Amber', 'precond - relationships work'); tag = store.createRecord('tag', { name: 'js' }); person.tags.push(tag); @@ -1288,8 +1291,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1384,8 +1387,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1464,7 +1467,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; + const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; assert.todo.deepEqual( pets.map((p) => p.id), @@ -1497,8 +1500,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1582,7 +1585,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; + const hasManyCanonical = person.hasMany('pets').hasManyRelationship.remoteState; assert.todo.deepEqual( pets.map((p) => p.id), @@ -1613,8 +1616,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:pet', Pet); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1709,8 +1712,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:dog', Dog); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1782,8 +1785,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:dog', Dog); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1855,8 +1858,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:dog', Dog); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; adapter.deleteRecord = () => { @@ -1933,7 +1936,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:person', Person); this.owner.register('model:car', Car); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -1957,8 +1960,8 @@ module('unit/model/relationships - hasMany', function (hooks) { ], }); - let person = store.peekRecord('person', 1); - let cars = person.cars; + const person = store.peekRecord('person', 1); + const cars = person.cars; assert.strictEqual(cars.length, 2); @@ -1981,117 +1984,126 @@ module('unit/model/relationships - hasMany', function (hooks) { the parent record's hasMany is a situation in which this limitation will be encountered should other local changes to the relationship still exist. */ - test('[ASSERTS KNOWN LIMITATION STILL EXISTS] returning new hasMany relationship info from a delete clears local state', async function (assert) { - assert.expect(4); + deprecatedTest( + '[ASSERTS KNOWN LIMITATION STILL EXISTS] returning new hasMany relationship info from a delete clears local state', + { + id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', + until: '6.0', + count: 1, + refactor: true, + }, + async function (assert) { + assert.expect(4); - const Person = Model.extend({ - name: attr('string'), - pets: hasMany('pet', { async: false, inverse: null }), - }); + const Person = Model.extend({ + name: attr('string'), + pets: hasMany('pet', { async: false, inverse: null }), + }); - const Pet = Model.extend({ - name: attr('string'), - person: belongsTo('person', { async: false, inverse: null }), - }); + 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); - let store = this.owner.lookup('service:store'); - let 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, - included: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [{ type: 'pet', id: '2' }], + adapter.shouldBackgroundReloadRecord = () => false; + adapter.deleteRecord = () => { + return Promise.resolve({ + data: null, + included: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Chris Thoburn', + }, + relationships: { + pets: { + data: [{ type: 'pet', id: '2' }], + }, }, }, - }, - ], - }); - }; + ], + }); + }; - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Chris Thoburn', - }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '2' }, - ], - }, - }, - }, - 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: '2' }, + ], + }, }, }, - { - 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 = await person.pets; + const person = store.peekRecord('person', '1'); + const pets = await person.pets; - const shen = store.peekRecord('pet', '1'); - const rebel = store.peekRecord('pet', '3'); + const shen = store.peekRecord('pet', '1'); + const rebel = store.peekRecord('pet', '3'); - assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); - assert.deepEqual( - pets.map((p) => p.id), - ['1', '2'], - 'precond - relationship has the correct pets to start' - ); + assert.strictEqual(shen.name, 'Shenanigans', 'precond - relationships work'); + assert.deepEqual( + pets.map((p) => p.id), + ['1', '2'], + 'precond - relationship has the correct pets to start' + ); - pets.push(rebel); - await settled(); + pets.push(rebel); + await settled(); - assert.deepEqual( - pets.map((p) => p.id), - ['1', '2', '3'], - 'precond2 - relationship now has the correct three pets' - ); + assert.deepEqual( + pets.map((p) => p.id), + ['1', '2', '3'], + 'precond2 - relationship now has the correct three pets' + ); - await shen.destroyRecord({}); - // were ember-data to now preserve local edits during a relationship push, this would be '2' - assert.deepEqual( - pets.map((p) => p.id), - ['2'], - 'relationship now has only one pet, we lost the local change' - ); - }); + await shen.destroyRecord({}); + // were ember-data to now preserve local edits during a relationship push, this would be 2 pets + assert.deepEqual( + pets.map((p) => p.id), + ['2'], // ['2', '3'], + 'we only have one pet' // 'relationship has two pets, we kept the local change' + ); + } + ); test('possible to replace items in a relationship using setObjects w/ Ember Enumerable Array/Object as the argument (GH-2533)', function (assert) { assert.expect(DEPRECATE_ARRAY_LIKE ? 3 : 2); @@ -2109,7 +2121,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -2141,7 +2153,7 @@ module('unit/model/relationships - hasMany', function (hooks) { type: 'tag', id: '1', attributes: { - name: 'ember', + name: 'Amber', }, }, { @@ -2154,8 +2166,8 @@ module('unit/model/relationships - hasMany', function (hooks) { ], }); - let tom = store.peekRecord('person', '1'); - let sylvain = store.peekRecord('person', '2'); + const tom = store.peekRecord('person', '1'); + const sylvain = store.peekRecord('person', '2'); // Test that since sylvain.tags instanceof ManyArray, // adding records on Relationship iterates correctly. if (DEPRECATE_ARRAY_LIKE) { @@ -2189,7 +2201,7 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -2222,11 +2234,11 @@ module('unit/model/relationships - hasMany', function (hooks) { ], }); - let tom = store.peekRecord('person', '1'); - let tag = store.peekRecord('tag', '2'); + const tom = store.peekRecord('person', '1'); + const tag = store.peekRecord('tag', '2'); assert.expectAssertion(() => { tom.tags.setObjects(tag); - }, /Assertion Failed: ManyArray.setObjects expects to receive an array as its argument/); + }, /ManyArray.setObjects expects to receive an array as its argument/); } ); @@ -2246,8 +2258,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -2270,15 +2282,15 @@ module('unit/model/relationships - hasMany', function (hooks) { type: 'tag', id: '1', attributes: { - name: 'ember', + name: 'Amber', }, }, ], }); - let tag = person.tags.at(0); + const tag = person.tags.at(0); - assert.strictEqual(tag.name, 'ember', 'precond - relationships work'); + assert.strictEqual(tag.name, 'Amber', 'precond - relationships work'); person.tags.splice(0, 1); @@ -2299,13 +2311,13 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.createRecord('person'); - let tag1 = store.createRecord('tag'); - let tag2 = store.createRecord('tag'); - let tag3 = store.createRecord('tag'); - let tags = person.tags; + const person = store.createRecord('person'); + const tag1 = store.createRecord('tag'); + const tag2 = store.createRecord('tag'); + const tag3 = store.createRecord('tag'); + const tags = person.tags; tags.push(tag1, tag2, tag3); tags.splice(tags.indexOf(tag2), 1); @@ -2338,10 +2350,10 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let tag = store.createRecord('tag'); + const store = this.owner.lookup('service:store'); + const tag = store.createRecord('tag'); - assert.ok(tag.people instanceof PromiseManyArray, 'people should be an async relationship'); + assert.ok(typeof tag.people.then === 'function', 'people should be an async relationship'); }); test('PromiseHasMany is stable', async function (assert) { @@ -2358,15 +2370,15 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let tag = store.createRecord('tag'); - let people = tag.people; - let peopleCached = tag.people; + const store = this.owner.lookup('service:store'); + const tag = store.createRecord('tag'); + const people = tag.people; + const peopleCached = tag.people; assert.strictEqual(people, peopleCached); tag.notifyPropertyChange('people'); - let notifiedPeople = tag.people; + const notifiedPeople = tag.people; assert.strictEqual(people, notifiedPeople); @@ -2387,14 +2399,13 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let tag = store.createRecord('tag'); - let peopleProxy = tag.people; - let people = await peopleProxy; + const store = this.owner.lookup('service:store'); + const tag = store.createRecord('tag'); + const peopleProxy = tag.people; + const people = await peopleProxy; tag.unloadRecord(); assert.true(people.isDestroying, 'people is destroying sync after unloadRecord'); - assert.true(peopleProxy.isDestroying, 'peopleProxy is destroying after the run post unloadRecord'); assert.true(peopleProxy.isDestroyed, 'peopleProxy is destroyed after the run post unloadRecord'); await settled(); @@ -2480,7 +2491,7 @@ module('unit/model/relationships - hasMany', function (hooks) { }, }); - let post = store.peekRecord('post', '1'); + const post = store.peekRecord('post', '1'); const promise = post.comments; const promise2 = post.comments; @@ -2655,8 +2666,8 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let tag = store.createRecord('tag'); + const store = this.owner.lookup('service:store'); + const tag = store.createRecord('tag'); tag.hasMany('people').hasManyRelationship; const support = LEGACY_SUPPORT.get(tag); const sync = support._syncArray; @@ -2678,7 +2689,7 @@ module('unit/model/relationships - hasMany', function (hooks) { 'expect people hasMany to not dirty after fetch completes, as we did not hit network' ); - let person = store.createRecord('person'); + const person = store.createRecord('person'); assert.strictEqual(peopleDidChange, 0, 'expect people hasMany to not sync before access'); people = await tag.people; @@ -2706,11 +2717,11 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.findHasMany = function (store, snapshot, url, relationship) { - assert.strictEqual(relationship.key, 'tags', 'relationship should be tags'); + assert.strictEqual(relationship.name, 'tags', 'relationship should be tags'); return { data: [ @@ -2771,9 +2782,9 @@ module('unit/model/relationships - hasMany', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let tag = store.createRecord('tag'); - let person = store.createRecord('person'); + const store = this.owner.lookup('service:store'); + const tag = store.createRecord('tag'); + const person = store.createRecord('person'); assert.expectAssertion(() => { tag.people = person; @@ -2782,43 +2793,42 @@ module('unit/model/relationships - hasMany', function (hooks) { await settled(); }); - deprecatedTest( - 'checks if passed array only contains instances of Model', - { - id: 'ember-data:deprecate-promise-proxies', - count: IS_DEPRECATE_MANY_ARRAY_DUPLICATES_4_12 ? 4 : 5, - until: '5.0', - }, - async function (assert) { - const Person = Model.extend(); - const Tag = Model.extend({ - people: hasMany('person', { async: true, inverse: null }), - }); + 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); + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); - adapter.findRecord = function () { - return { - data: { - type: 'person', - id: '1', - }, - }; + adapter.findRecord = function () { + return { + data: { + type: 'person', + id: '1', + }, }; + }; - let tag = store.createRecord('tag'); - let person = store.findRecord('person', '1'); - await person; + const tag = store.createRecord('tag'); + const person = store.findRecord('person', '1'); + await person; - tag.people = [person]; + tag.people = [person]; - assert.expectAssertion(() => { - tag.people = [person, {}]; - }, /All elements of a hasMany relationship must be instances of Model/); - } - ); + 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/model/relationships/record-array-test.js b/tests/main/tests/unit/model/relationships/record-array-test.js index 8d0aa8ab456..ae664cf5af9 100644 --- a/tests/main/tests/unit/model/relationships/record-array-test.js +++ b/tests/main/tests/unit/model/relationships/record-array-test.js @@ -6,7 +6,7 @@ import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -module('unit/model/relationships - RecordArray', function (hooks) { +module('unit/model/relationships - ManyArray', function (hooks) { setupTest(hooks); test('can create child record from a hasMany relationship', async function (assert) { @@ -25,8 +25,8 @@ module('unit/model/relationships - RecordArray', function (hooks) { this.owner.register('model:tag', Tag); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; @@ -40,7 +40,7 @@ module('unit/model/relationships - RecordArray', function (hooks) { }, }); - let person = await store.findRecord('person', 1); + const person = await store.findRecord('person', 1); person.tags.createRecord({ name: 'cool' }); assert.strictEqual(get(person, 'name'), 'Tom Dale', 'precond - retrieves person record from store'); diff --git a/tests/main/tests/unit/model/rollback-attributes-test.js b/tests/main/tests/unit/model/rollback-attributes-test.js index 55595584d06..18573be5b24 100644 --- a/tests/main/tests/unit/model/rollback-attributes-test.js +++ b/tests/main/tests/unit/model/rollback-attributes-test.js @@ -1,9 +1,7 @@ import { addObserver } from '@ember/object/observers'; -import { run } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { reject } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -30,8 +28,8 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h }); test('changes to attributes can be rolled back', function (assert) { - let store = this.owner.lookup('service:store'); - let person = store.push({ + const store = this.owner.lookup('service:store'); + const person = store.push({ data: { type: 'person', id: '1', @@ -52,28 +50,23 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h }); test('changes to unassigned attributes can be rolled back', function (assert) { - let store = this.owner.lookup('service:store'); - let person; - - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - lastName: 'Dale', - }, - }, - }); - person = store.peekRecord('person', 1); - person.set('firstName', 'Thomas'); + const store = this.owner.lookup('service:store'); - return person; + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + lastName: 'Dale', + }, + }, }); + const person = store.peekRecord('person', 1); + person.set('firstName', 'Thomas'); assert.strictEqual(person.firstName, 'Thomas'); - run(() => person.rollbackAttributes()); + person.rollbackAttributes(); assert.strictEqual(person.firstName, undefined); assert.false(person.hasDirtyAttributes); @@ -84,7 +77,7 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h const adapter = store.adapterFor('application'); let resolve; - let trap = new Promise((r) => (resolve = r)); + const trap = new Promise((r) => (resolve = r)); adapter.updateRecord = async function (store, type, snapshot) { resolve(); await trap; @@ -127,14 +120,14 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h }); test("a record's changes can be made if it fails to save", async function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.updateRecord = function (store, type, snapshot) { - return reject(); + return Promise.reject(); }; - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -163,67 +156,61 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h } }); - test(`a deleted record's attributes can be rollbacked if it fails to save, record arrays are updated accordingly`, function (assert) { - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + test(`a deleted record's attributes can be rollbacked if it fails to save, record arrays are updated accordingly`, async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); adapter.deleteRecord = function (store, type, snapshot) { - return reject(); + return Promise.reject(); }; - let person, people; - - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Tom', - lastName: 'Dale', - }, + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Tom', + lastName: 'Dale', }, - }); - person = store.peekRecord('person', 1); - people = store.peekAll('person'); + }, }); + const person = store.peekRecord('person', 1); + const people = store.peekAll('person'); - run(() => person.deleteRecord()); + person.deleteRecord(); assert.strictEqual(people.length, 1, 'a deleted record appears in record array until it is saved'); assert.strictEqual(people.at(0), person, 'a deleted record appears in record array until it is saved'); - return run(() => { - return person - .save() - .catch(() => { - assert.true(person.isError); - assert.true(person.isDeleted); - - run(() => person.rollbackAttributes()); - - assert.false(person.isDeleted); - assert.false(person.isError); - assert.false(person.hasDirtyAttributes, 'must be not dirty'); - }) - .then(() => { - assert.strictEqual( - people.length, - 1, - 'the underlying record array is updated accordingly in an asynchronous way' - ); - }); - }); + await person + .save() + .catch(() => { + assert.true(person.isError); + assert.true(person.isDeleted); + + person.rollbackAttributes(); + + assert.false(person.isDeleted); + assert.false(person.isError); + assert.false(person.hasDirtyAttributes, 'must be not dirty'); + }) + .then(() => { + assert.strictEqual( + people.length, + 1, + 'the underlying record array is updated accordingly in an asynchronous way' + ); + }); }); test(`new record's attributes can be rollbacked`, function (assert) { - let store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { id: '1' }); + const store = this.owner.lookup('service:store'); + const person = store.createRecord('person', { id: '1' }); assert.true(person.isNew, 'must be new'); assert.true(person.hasDirtyAttributes, 'must be dirty'); - run(person, 'rollbackAttributes'); + person.rollbackAttributes(); assert.false(person.isNew, 'must not be new'); assert.false(person.hasDirtyAttributes, 'must not be dirty'); @@ -231,24 +218,24 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h }); test(`invalid new record's attributes can be rollbacked`, async function (assert) { - let error = new InvalidError([ + const error = new InvalidError([ { detail: 'is invalid', source: { pointer: 'data/attributes/name' }, }, ]); - let adapter = RESTAdapter.extend({ + const adapter = RESTAdapter.extend({ ajax(url, type, hash) { - return reject(error); + return Promise.reject(error); }, }); this.owner.register('adapter:application', adapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { id: '1' }); + const store = this.owner.lookup('service:store'); + const person = store.createRecord('person', { id: '1' }); assert.true(person.isNew, 'must be new'); assert.true(person.hasDirtyAttributes, 'must be dirty'); @@ -267,81 +254,72 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h } }); - test(`invalid record's attributes can be rollbacked after multiple failed calls - #3677`, function (assert) { - let adapter = RESTAdapter.extend({ + test(`invalid record's attributes can be rollbacked after multiple failed calls - #3677`, async function (assert) { + const adapter = RESTAdapter.extend({ ajax(url, type, hash) { - let error = new InvalidError(); - return reject(error); + const error = new InvalidError(); + return Promise.reject(error); }, }); this.owner.register('adapter:application', adapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - - let person; - run(() => { - person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'original name', - }, - }, - }); + const store = this.owner.lookup('service:store'); - person.set('firstName', 'updated name'); + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'original name', + }, + }, }); - return run(() => { - assert.strictEqual(person.firstName, 'updated name', 'precondition: firstName is changed'); - - return person - .save() - .catch(() => { - assert.true(person.hasDirtyAttributes, 'has dirty attributes'); - assert.strictEqual(person.firstName, 'updated name', 'firstName is still changed'); - - return person.save(); - }) - .catch(() => { - run(() => person.rollbackAttributes()); - - assert.false(person.hasDirtyAttributes, 'has no dirty attributes'); - assert.strictEqual( - person.firstName, - 'original name', - 'after rollbackAttributes() firstName has the original value' - ); - }); - }); - }); + person.set('firstName', 'updated name'); - test(`deleted record's attributes can be rollbacked`, function (assert) { - let store = this.owner.lookup('service:store'); + assert.strictEqual(person.firstName, 'updated name', 'precondition: firstName is changed'); - let person, people; + await person + .save() + .catch(() => { + assert.true(person.hasDirtyAttributes, 'has dirty attributes'); + assert.strictEqual(person.firstName, 'updated name', 'firstName is still changed'); - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - }, + return person.save(); + }) + .catch(() => { + person.rollbackAttributes(); + + assert.false(person.hasDirtyAttributes, 'has no dirty attributes'); + assert.strictEqual( + person.firstName, + 'original name', + 'after rollbackAttributes() firstName has the original value' + ); }); - person = store.peekRecord('person', 1); - people = store.peekAll('person'); - person.deleteRecord(); + }); + + test(`deleted record's attributes can be rollbacked`, function (assert) { + const store = this.owner.lookup('service:store'); + + store.push({ + data: { + type: 'person', + id: '1', + }, }); + const person = store.peekRecord('person', 1); + const people = store.peekAll('person'); + person.deleteRecord(); assert.strictEqual(people.length, 1, 'a deleted record appears in the record array until it is saved'); assert.strictEqual(people.at(0), person, 'a deleted record appears in the record array until it is saved'); assert.true(person.isDeleted, 'must be deleted'); - run(() => person.rollbackAttributes()); + person.rollbackAttributes(); assert.strictEqual(people.length, 1, 'the rollbacked record should appear again in the record array'); assert.false(person.isDeleted, 'must not be deleted'); @@ -360,7 +338,7 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h ]); class TestAdapter extends RESTAdapter { ajax() { - return reject(thrownAdapterError); + return Promise.reject(thrownAdapterError); } } @@ -415,7 +393,7 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h ]); class TestAdapter extends RESTAdapter { ajax() { - return reject(thrownAdapterError); + return Promise.reject(thrownAdapterError); } } const { owner } = this; @@ -472,16 +450,16 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h name: attr(), }); - let error = new InvalidError([ + const error = new InvalidError([ { detail: 'is invalid', source: { pointer: 'data/attributes/name' }, }, ]); - let adapter = RESTAdapter.extend({ + const adapter = RESTAdapter.extend({ ajax(url, type, hash) { - return reject(error); + return Promise.reject(error); }, }); @@ -489,8 +467,8 @@ module('unit/model/rollbackAttributes - model.rollbackAttributes()', function (h this.owner.register('adapter:application', adapter); this.owner.register('serializer:application', RESTSerializer.extend()); - let store = this.owner.lookup('service:store'); - let dog = store.push({ + const store = this.owner.lookup('service:store'); + const dog = store.push({ data: { type: 'dog', id: '1', diff --git a/tests/main/tests/unit/promise-proxies-test.js b/tests/main/tests/unit/promise-proxies-test.js index 8b6cada952c..5283e7204aa 100644 --- a/tests/main/tests/unit/promise-proxies-test.js +++ b/tests/main/tests/unit/promise-proxies-test.js @@ -1,7 +1,6 @@ import { A } from '@ember/array'; import { module, test } from 'qunit'; -import { Promise as EmberPromise } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -14,15 +13,15 @@ module('PromiseManyArray', function () { test('.reload should NOT leak the internal promise, rather return another promiseArray', function (assert) { assert.expect(1); - let content = A(); + const content = A(); - content.reload = () => EmberPromise.resolve(content); + content.reload = () => Promise.resolve(content); - let array = PromiseManyArray.create({ + const array = PromiseManyArray.create({ content, }); - let reloaded = array.reload(); + const reloaded = array.reload(); assert.strictEqual(reloaded, array); }); @@ -30,17 +29,16 @@ module('PromiseManyArray', function () { test('.reload should be stable', async function (assert) { assert.expect(19); - let content = A(); - let array; + const content = A(); content.reload = () => { - let p = EmberPromise.resolve(content); + const p = Promise.resolve(content); array._update(p); return p; }; - let promise = EmberPromise.resolve(content); + const promise = Promise.resolve(content); - array = PromiseManyArray.create({ + const array = PromiseManyArray.create({ promise, }); @@ -55,7 +53,7 @@ module('PromiseManyArray', function () { assert.true(array.isSettled, 'should be settled'); assert.true(array.isFulfilled, 'should be fulfilled'); - let reloaded = array.reload(); + const reloaded = array.reload(); assert.false(array.isRejected, 'should NOT be rejected'); assert.true(array.isPending, 'should be pending'); @@ -65,7 +63,7 @@ module('PromiseManyArray', function () { assert.ok(reloaded instanceof PromiseManyArray); assert.strictEqual(reloaded, array); - let value = await reloaded; + const value = await reloaded; assert.false(array.isRejected, 'should NOT be rejected'); assert.false(array.isPending, 'should NOT be pending'); assert.true(array.isSettled, 'should be settled'); @@ -77,11 +75,11 @@ module('PromiseManyArray', function () { test('.set to new promise should be like reload', async function (assert) { assert.expect(18); - let content = A([1, 2, 3]); + const content = A([1, 2, 3]); - let promise = EmberPromise.resolve(content); + const promise = Promise.resolve(content); - let array = PromiseManyArray.create({ + const array = PromiseManyArray.create({ promise, }); @@ -96,7 +94,7 @@ module('PromiseManyArray', function () { assert.true(array.isSettled, 'should be settled'); assert.true(array.isFulfilled, 'should be fulfilled'); - array._update(EmberPromise.resolve(content)); + array._update(Promise.resolve(content)); assert.false(array.isRejected, 'should NOT be rejected'); assert.true(array.isPending, 'should be pending'); @@ -105,7 +103,7 @@ module('PromiseManyArray', function () { assert.ok(array instanceof PromiseManyArray); - let value = await array; + const value = await array; assert.false(array.isRejected, 'should NOT be rejected'); assert.false(array.isPending, 'should NOT be pending'); assert.true(array.isSettled, 'should be settled'); @@ -142,7 +140,7 @@ module('unit/PromiseBelongsTo', function (hooks) { }, }, }; - return EmberPromise.resolve(ChildRecord); + return Promise.resolve(ChildRecord); } } @@ -174,9 +172,14 @@ module('unit/PromiseBelongsTo', function (hooks) { const belongsToProxy = parent.child; - assert.expectAssertion(() => { - belongsToProxy.meta; - }, 'You attempted to access meta on the promise for the async belongsTo relationship ' + `child:child'.` + '\nUse `record.belongsTo(relationshipName).meta()` instead.'); + assert.expectAssertion( + () => { + belongsToProxy.meta; + }, + 'You attempted to access meta on the promise for the async belongsTo relationship ' + + `child:child'.` + + '\nUse `record.belongsTo(relationshipName).meta()` instead.' + ); assert.strictEqual(parent.belongsTo('child').meta(), meta); await belongsToProxy; 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 70c457c1af9..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 @@ -1,10 +1,10 @@ import { module, skip, test } from 'qunit'; -import RSVP from 'rsvp'; import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; -import { AdapterPopulatedRecordArray, RecordArrayManager, SOURCE } from '@ember-data/store/-private'; +import { createDeferred } from '@ember-data/request'; +import { CollectionRecordArray, RecordArrayManager, SOURCE } from '@ember-data/store/-private'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Tag extends Model { @@ -12,11 +12,11 @@ class Tag extends Model { name; } -module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedRecordArray', function (hooks) { +module('unit/record-arrays/collection', function (hooks) { setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = new AdapterPopulatedRecordArray({ + const recordArray = new CollectionRecordArray({ type: 'recordType', isLoaded: false, identifiers: [], @@ -32,8 +32,8 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR }); test('custom initial state', async function (assert) { - let store = {}; - let recordArray = new AdapterPopulatedRecordArray({ + const store = {}; + const recordArray = new CollectionRecordArray({ type: 'apple', isLoaded: true, identifiers: ['1'], @@ -51,21 +51,32 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR }); testInDebug('#replace() throws error', function (assert) { - let recordArray = new AdapterPopulatedRecordArray({ type: 'recordType', identifiers: [] }); + const recordArray = new CollectionRecordArray({ type: 'recordType', identifiers: [] }); assert.throws( () => { recordArray.replace(); }, - Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), + 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: [] }); + + assert.throws( + () => { + recordArray.splice(0, 1); + }, + Error('Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + }); test('#update uses _update enabling query specific behavior', async function (assert) { let queryCalled = 0; - let deferred = RSVP.defer(); + const deferred = createDeferred(); const store = { query(modelName, query, options) { @@ -78,7 +89,7 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR }, }; - let recordArray = new AdapterPopulatedRecordArray({ + const recordArray = new CollectionRecordArray({ type: 'recordType', store, identifiers: [], @@ -90,7 +101,7 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR assert.strictEqual(queryCalled, 0); - let updateResult = recordArray.update(); + const updateResult = recordArray.update(); assert.strictEqual(queryCalled, 1); const expectedResult = []; @@ -110,26 +121,26 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let contentDidChange = 0; this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let manager = new RecordArrayManager({ + const manager = new RecordArrayManager({ store, }); - let recordArray = new AdapterPopulatedRecordArray({ + const recordArray = new CollectionRecordArray({ query: 'some-query', manager, identifiers: [], store, }); - let model1 = { + const model1 = { type: 'tag', id: '1', attributes: { name: 'Scumbag Dale', }, }; - let model2 = { + const model2 = { type: 'tag', id: '2', attributes: { @@ -174,14 +185,14 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR arrayDidChange = 0; contentDidChange = 0; - let model3 = { + const model3 = { type: 'tag', id: '3', attributes: { name: 'Scumbag Penner', }, }; - let model4 = { + const model4 = { type: 'tag', id: '4', attributes: { @@ -225,7 +236,7 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR assert.strictEqual(arrayDidChange, 0, 'record array should not yet have omitted a change event'); assert.strictEqual(contentDidChange, 0, 'recordArray.content should not have changed'); - let model5 = { + const model5 = { type: 'tag', id: '5', attributes: { 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 6855c089b23..56dc55d4989 100644 --- a/tests/main/tests/unit/record-arrays/record-array-test.js +++ b/tests/main/tests/unit/record-arrays/record-array-test.js @@ -1,12 +1,12 @@ import { module, test } from 'qunit'; -import RSVP from 'rsvp'; import { setupTest } from 'ember-qunit'; import { FetchManager, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import Model, { attr } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import { recordIdentifierFor } from '@ember-data/store'; -import { RecordArray, SOURCE } from '@ember-data/store/-private'; +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'; @@ -15,11 +15,11 @@ class Tag extends Model { name; } -module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { +module('unit/record-arrays/live-array - LiveArray', function (hooks) { setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = new RecordArray({ type: 'recordType', identifiers: [], store: null }); + const recordArray = new LiveArray({ type: 'recordType', identifiers: [], store: null }); assert.false(recordArray.isUpdating, 'record is not updating'); assert.strictEqual(recordArray.modelName, 'recordType', 'has modelName'); @@ -28,8 +28,8 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }); test('custom initial state', async function (assert) { - let store = {}; - let recordArray = new RecordArray({ + const store = {}; + const recordArray = new LiveArray({ type: 'apple', identifiers: [], store, @@ -41,35 +41,35 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }); testInDebug('#replace() throws error', async function (assert) { - let recordArray = new RecordArray({ identifiers: [], type: 'recordType' }); + const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); assert.throws( () => { recordArray.replace(); }, - Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), + 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) { - let recordArray = new RecordArray({ identifiers: [], type: 'recordType' }); + const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); assert.throws( () => { recordArray.splice(0, 1); }, - Error('Assertion Failed: Mutating this array of records via splice is not allowed.'), + Error('Mutating this array of records via splice is not allowed.'), 'throws error' ); }); test('#access by index', async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -86,7 +86,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -104,9 +104,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -129,7 +129,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -144,9 +144,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { deprecatedTest('#reject', { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -169,7 +169,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -186,9 +186,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -211,7 +211,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -229,9 +229,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { { id: 'ember-data:deprecate-array-like', until: '5.0', count: 2 }, async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -254,7 +254,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -271,9 +271,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { { id: 'ember-data:deprecate-array-like', until: '5.0', count: 5 }, async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let records = store.push({ + const records = store.push({ data: [ { type: 'tag', @@ -296,7 +296,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { ], }); - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: records.map(recordIdentifierFor), store, @@ -314,7 +314,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { test('#update', async function (assert) { let findAllCalled = 0; - let deferred = RSVP.defer(); + const deferred = createDeferred(); const store = { findAll(modelName, options) { @@ -325,7 +325,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }, }; - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: [], store, @@ -335,7 +335,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { assert.strictEqual(findAllCalled, 0); - let updateResult = recordArray.update(); + const updateResult = recordArray.update(); assert.strictEqual(findAllCalled, 1); @@ -351,7 +351,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { test('#update while updating', async function (assert) { let findAllCalled = 0; - let deferred = RSVP.defer(); + const deferred = createDeferred(); const store = { findAll(modelName, options) { findAllCalled++; @@ -359,7 +359,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { }, }; - let recordArray = new RecordArray({ + const recordArray = new LiveArray({ type: 'recordType', identifiers: [], store, @@ -368,11 +368,11 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { assert.false(recordArray.isUpdating, 'should not be updating'); assert.strictEqual(findAllCalled, 0); - let updateResult1 = recordArray.update(); + const updateResult1 = recordArray.update(); assert.strictEqual(findAllCalled, 1); - let updateResult2 = recordArray.update(); + const updateResult2 = recordArray.update(); assert.strictEqual(findAllCalled, 1); @@ -390,22 +390,22 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { test('#save', async function (assert) { this.owner.register('model:tag', Tag); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let model1 = { + const model1 = { id: '1', type: 'tag', }; - let model2 = { + const model2 = { id: '2', type: 'tag', }; - let [record1, record2] = store.push({ + const [record1, record2] = store.push({ data: [model1, model2], }); - let identifiers = [recordIdentifierFor(record1), recordIdentifierFor(record2)]; - let recordArray = new RecordArray({ + const identifiers = [recordIdentifierFor(record1), recordIdentifierFor(record2)]; + const recordArray = new LiveArray({ identifiers, store, }); @@ -420,7 +420,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { assert.strictEqual(model1Saved, 0, 'save not yet called'); assert.strictEqual(model2Saved, 0, 'save not yet called'); - let result = recordArray.save(); + const result = recordArray.save(); assert.strictEqual(model1Saved, 1, 'save was called for model1'); assert.strictEqual(model2Saved, 1, 'save was called for mode2'); @@ -434,12 +434,12 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { const store = this.owner.lookup('service:store'); store._fetchManager = new FetchManager(store); - let model1 = { + const model1 = { id: '1', type: 'tag', }; - let model2 = { + const model2 = { id: '2', type: 'tag', }; @@ -447,8 +447,8 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { data: [model1, model2], }); - let snapshot = new SnapshotRecordArray(store, 'tag', {}); - let [snapshot1, snapshot2] = snapshot.snapshots(); + const snapshot = new SnapshotRecordArray(store, 'tag', {}); + const [snapshot1, snapshot2] = snapshot.snapshots(); assert.strictEqual( snapshot1.id, diff --git a/tests/main/tests/unit/store/adapter-interop-test.js b/tests/main/tests/unit/store/adapter-interop-test.js index 26a3f8bffdd..29585f9502d 100644 --- a/tests/main/tests/unit/store/adapter-interop-test.js +++ b/tests/main/tests/unit/store/adapter-interop-test.js @@ -1,9 +1,6 @@ -import { get, set } from '@ember/object'; -import { later } from '@ember/runloop'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { all, Promise as EmberPromise, resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -19,7 +16,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho test('Calling Store#find invokes its adapter#find', async function (assert) { assert.expect(5); - let currentStore = this.owner.lookup('service:store'); + const currentStore = this.owner.lookup('service:store'); const ApplicationAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { @@ -33,7 +30,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho assert.strictEqual(id, '1', 'Adapter#find was called with the id passed into Store#find'); assert.strictEqual(snapshot.id, '1', 'Adapter#find was called with the record created from Store#find'); - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'test', @@ -59,7 +56,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho findMany(store, type, ids, snapshots) { assert.ok(true, 'Adapter#findMany was called'); assert.deepEqual(ids, ['1', '2'], 'Correct ids were passed in to findMany'); - return resolve({ + return Promise.resolve({ data: [ { id: '1', type: 'test' }, { id: '2', type: 'test' }, @@ -73,9 +70,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('model:test', Model.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - await all([store.findRecord('test', '1'), store.findRecord('test', '2')]); + await Promise.all([store.findRecord('test', '1'), store.findRecord('test', '2')]); }); test('Coalesced Store#findRecord requests retain the `include` adapter option in the snapshots passed to adapter#findMany and adapter#findRecord', async function (assert) { @@ -107,7 +104,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho assert.deepEqual(snapshot.adapterOptions, options[snapshot.id], 'we were passed the right adapterOptions'); }); assert.deepEqual(ids, ['1', '2'], 'we were passed the expected ids'); - return resolve({ + return Promise.resolve({ data: snapshots.map(({ id }) => ({ id, type: type.modelName })), }); }, @@ -121,7 +118,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho assert.deepEqual(snapshot.adapterOptions, options[snapshot.id], 'we were passed the right adapterOptions'); assert.strictEqual(id, '3', 'we were passed the expected id'); - return resolve({ + return Promise.resolve({ data: { id, type: type.modelName }, }); }, @@ -133,9 +130,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('model:test', Model.extend()); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - await all( + await Promise.all( Object.keys(includedResourcesForIds).map((id) => store.findRecord('test', id, { include: includedResourcesForIds[id], adapterOptions: options[id] }) ) @@ -147,7 +144,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho const ApplicationAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { - return resolve({ data: { id: '1', type: 'test', attributes: { name: 'Scumbag Dale' } } }); + return Promise.resolve({ data: { id: '1', type: 'test', attributes: { name: 'Scumbag Dale' } } }); }, }); @@ -155,10 +152,10 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('model:test', Model.extend({ name: attr() })); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findRecord('test', '1').then((object) => { - assert.strictEqual(get(object, 'name'), 'Scumbag Dale', 'the data was pushed'); + assert.strictEqual(object.name, 'Scumbag Dale', 'the data was pushed'); }); }); @@ -168,7 +165,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho const ApplicationAdapter = Adapter.extend({ findRecord(store, type, id, snapshot) { assert.strictEqual(typeof id, 'string', 'id has been normalized to a string'); - return resolve({ data: { id, type: 'test', attributes: { name: 'Scumbag Sylvain' } } }); + return Promise.resolve({ data: { id, type: 'test', attributes: { name: 'Scumbag Sylvain' } } }); }, }); @@ -176,7 +173,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('model:test', Model.extend({ name: attr() })); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store .findRecord('test', 1) @@ -221,7 +218,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -234,8 +231,8 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); await store.findRecord('person', '1').then((tom) => { - assert.false(get(tom, 'hasDirtyAttributes'), 'precond - record is not dirty'); - assert.strictEqual(get(tom, 'name'), 'Tom Dale', 'returns the correct name'); + assert.false(tom.hasDirtyAttributes, 'precond - record is not dirty'); + assert.strictEqual(tom.name, 'Tom Dale', 'returns the correct name'); store.push({ data: { @@ -246,14 +243,14 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, }, }); - assert.strictEqual(get(tom, 'name'), 'Captain Underpants', 'updated record with new date'); + assert.strictEqual(tom.name, 'Captain Underpants', 'updated record with new date'); }); }); test('loadMany takes an optional Object and passes it on to the Adapter', async function (assert) { assert.expect(2); - let passedQuery = { page: 1 }; + const passedQuery = { page: 1 }; const Person = Model.extend({ name: attr('string'), @@ -263,7 +260,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho query(store, type, query) { assert.strictEqual(type, store.modelFor('person'), 'The type was Person'); assert.strictEqual(query, passedQuery, 'The query was passed in'); - return resolve({ data: [] }); + return Promise.resolve({ data: [] }); }, }); @@ -271,13 +268,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.query('person', passedQuery); }); test('Find with query calls the correct normalizeResponse', async function (assert) { - let passedQuery = { page: 1 }; + const passedQuery = { page: 1 }; let callCount = 0; const Person = Model.extend({ @@ -286,7 +283,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho const ApplicationAdapter = Adapter.extend({ query(store, type, query) { - return resolve([]); + return Promise.resolve([]); }, }); @@ -301,7 +298,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', ApplicationSerializer); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.query('person', passedQuery); @@ -316,7 +313,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('model:person', Person); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -328,9 +325,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, }); - let results = store.peekAll('person'); + const results = store.peekAll('person'); - assert.strictEqual(get(results, 'length'), 1, 'record array should have the original object'); + assert.strictEqual(results.length, 1, 'record array should have the original object'); assert.strictEqual(results.at(0).name, 'Tom Dale', 'record has the correct information'); store.push({ @@ -356,15 +353,15 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('model:person', Person); - let person = this.owner.lookup('service:store').createRecord('person'); + const person = this.owner.lookup('service:store').createRecord('person'); - assert.true(get(person, 'isLoaded'), 'A newly created record is loaded'); - assert.true(get(person, 'isNew'), 'A newly created record is new'); - assert.true(get(person, 'hasDirtyAttributes'), 'A newly created record is dirty'); + assert.true(person.isLoaded, 'A newly created record is loaded'); + assert.true(person.isNew, 'A newly created record is new'); + assert.true(person.hasDirtyAttributes, 'A newly created record is dirty'); - set(person, 'name', 'Braaahm Dale'); + person.name = 'Braaahm Dale'; - assert.strictEqual(get(person, 'name'), 'Braaahm Dale', 'Even if no hash is supplied, `set` still worked'); + assert.strictEqual(person.name, 'Braaahm Dale', 'Even if no hash is supplied, `set` still worked'); }); testInDebug( @@ -376,7 +373,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.createRecord('person', { id: '5' }); @@ -393,14 +390,14 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { name: 'Brohuda Katz' }); + const store = this.owner.lookup('service:store'); + const person = store.createRecord('person', { name: 'Brohuda Katz' }); - assert.true(get(person, 'isLoaded'), 'A newly created record is loaded'); - assert.true(get(person, 'isNew'), 'A newly created record is new'); - assert.true(get(person, 'hasDirtyAttributes'), 'A newly created record is dirty'); + assert.true(person.isLoaded, 'A newly created record is loaded'); + assert.true(person.isNew, 'A newly created record is new'); + assert.true(person.hasDirtyAttributes, 'A newly created record is dirty'); - assert.strictEqual(get(person, 'name'), 'Brohuda Katz', 'The initial data hash is provided'); + assert.strictEqual(person.name, 'Brohuda Katz', 'The initial data hash is provided'); }); test('if an id is supplied in the initial data hash, it can be looked up using `store.find`', async function (assert) { @@ -417,9 +414,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('model:person', Person); this.owner.register('adapter:application', ApplicationAdapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.createRecord('person', { id: '1', name: 'Brohuda Katz' }); + const person = store.createRecord('person', { id: '1', name: 'Brohuda Katz' }); await store.findRecord('person', '1').then((again) => { assert.strictEqual(person, again, 'the store returns the loaded object'); @@ -444,7 +441,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findRecord('test', '1', { preload: { name: 'Test' } }); }); @@ -468,7 +465,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -480,7 +477,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, }); - let tom = store.peekRecord('person', 2); + const tom = store.peekRecord('person', 2); await store.findRecord('person', 1, { preload: { friend: tom } }); }); @@ -502,9 +499,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - await store.findRecord('person', '1', { preload: { friend: 2 } }).then(() => { + await store.findRecord('person', '1', { preload: { friend: '2' } }).then(() => { return store.peekRecord('person', '1').friend.then((friend) => { assert.strictEqual(friend.id, '2', 'Preloaded belongsTo set'); }); @@ -530,7 +527,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -542,7 +539,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, }); - let tom = store.peekRecord('person', '2'); + const tom = store.peekRecord('person', '2'); await store.findRecord('person', '1', { preload: { friends: [tom] } }); }); @@ -565,9 +562,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - await store.findRecord('person', '1', { preload: { friends: [2] } }); + await store.findRecord('person', '1', { preload: { friends: ['2'] } }); }); test('initial empty values of hasMany can be passed in as the third argument to find as records', async function (assert) { @@ -589,12 +586,12 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findRecord('person', '1', { preload: { friends: [] } }); }); - test('initial values of hasMany can be passed in as the third argument to find as ids', async function (assert) { + test('initial values of hasMany can be passed in as the third argument to find as ids, v2', async function (assert) { assert.expect(1); const Person = Model.extend({ @@ -613,9 +610,9 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - await store.findRecord('person', 1, { preload: { friends: [] } }); + await store.findRecord('person', '1', { preload: { friends: [] } }); }); test('records should have their ids updated when the adapter returns the id data', async function (assert) { @@ -644,13 +641,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let people = store.peekAll('person'); - let tom = store.createRecord('person', { name: 'Tom Dale' }); - let yehuda = store.createRecord('person', { name: 'Yehuda Katz' }); + const people = store.peekAll('person'); + const tom = store.createRecord('person', { name: 'Tom Dale' }); + const yehuda = store.createRecord('person', { name: 'Yehuda Katz' }); - await all([tom.save(), yehuda.save()]).then(() => { + await Promise.all([tom.save(), yehuda.save()]).then(() => { people.forEach((person, index) => { assert.strictEqual(person.id, String(index + 1), `The record's id should be correct.`); }); @@ -671,7 +668,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, findMany(store, type, ids, snapshots) { - let records = ids.map((id) => ({ id, type: 'test' })); + const records = ids.map((id) => ({ id, type: 'test' })); assert.deepEqual(ids, ['20', '21'], 'The second group is passed to findMany'); @@ -683,17 +680,17 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let identifiers = [ + const identifiers = [ store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '10' }), store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '20' }), store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '21' }), ]; - const result = await all(identifiers.map((id) => store.findRecord(id))); + const result = await Promise.all(identifiers.map((id) => store.findRecord(id))); - let ids = result.map((x) => x.id); + const ids = result.map((x) => x.id); assert.deepEqual(ids, ['10', '20', '21'], 'The promise fulfills with the identifiers'); }); @@ -708,16 +705,16 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, findRecord(store, type, id, snapshot) { - let record = { id, type: 'test' }; + const record = { id, type: 'test' }; - return new EmberPromise(function (resolve, reject) { + return new Promise(function (resolve, reject) { if (id === 'igor') { resolve({ data: record }); } else { - later(function () { + setTimeout(function () { davidResolved = true; resolve({ data: record }); - }, 5); + }, 1); } }); }, @@ -727,11 +724,11 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('adapter:application', ApplicationAdapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let david = store.findRecord('test', 'david'); - let igor = store.findRecord('test', 'igor'); - let wait = []; + const david = store.findRecord('test', 'david'); + const igor = store.findRecord('test', 'igor'); + const wait = []; wait.push( igor.then(() => { @@ -745,7 +742,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }) ); - await all(wait); + await Promise.all(wait); }); test('the promise returned by `findRecord`, when it rejects, does not depend on the promises returned to other calls to `findRecord` that are in the same run loop, but different groups', async function (assert) { @@ -759,13 +756,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }, findRecord(store, type, id, snapshot) { - let record = { id, type: 'test' }; + const record = { id, type: 'test' }; - return new EmberPromise((resolve, reject) => { + return new Promise((resolve, reject) => { if (id === 'igor') { reject({ data: record }); } else { - later(() => { + setTimeout(() => { davidResolved = true; resolve({ data: record }); }, 5); @@ -778,11 +775,11 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('serializer:application', class extends JSONAPISerializer {}); this.owner.register('adapter:application', ApplicationAdapter); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let david = store.findRecord('test', 'david'); - let igor = store.findRecord('test', 'igor'); - let wait = []; + const david = store.findRecord('test', 'david'); + const igor = store.findRecord('test', 'igor'); + const wait = []; wait.push( igor.catch(() => { @@ -796,7 +793,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }) ); - await EmberPromise.all(wait); + await Promise.all(wait); }); testInDebug( @@ -806,7 +803,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho const ApplicationAdapter = Adapter.extend({ findMany(store, type, ids, snapshots) { - let records = ids.map((id) => ({ id, type: 'test' })); + const records = ids.map((id) => ({ id, type: 'test' })); return { data: [records[0]] }; }, }); @@ -815,11 +812,11 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await assert.expectWarning(async () => { - let david = store.findRecord('test', 'david'); - let igor = store.findRecord('test', 'igor'); + const david = store.findRecord('test', 'david'); + const igor = store.findRecord('test', 'igor'); try { await Promise.all([david, igor]); @@ -838,7 +835,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho assert.expect(3); const ApplicationAdapter = Adapter.extend({ findMany(store, type, ids, snapshots) { - let records = ids.map((id) => ({ id, type: 'test' })).filter(({ id }) => id === 'david'); + const records = ids.map((id) => ({ id, type: 'test' })).filter(({ id }) => id === 'david'); return { data: [records[0]] }; }, @@ -848,13 +845,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let igorDidReject = true; await assert.expectWarning( async () => { - let wait = []; + const wait = []; wait.push(store.findRecord('test', 'david')); wait.push( store.findRecord('test', 'igor').catch((e) => { @@ -895,7 +892,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findRecord('person', '1'); }); @@ -920,7 +917,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -950,7 +947,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -988,7 +985,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -1020,7 +1017,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: { @@ -1058,7 +1055,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); const record = store.push({ data: { @@ -1093,7 +1090,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findAll('person'); }); @@ -1120,7 +1117,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findAll('person').then((records) => { assert.strictEqual(records.at(0).name, 'Tom'); @@ -1151,7 +1148,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findAll('person').then((records) => { assert.strictEqual(records[0].name, 'Tom'); @@ -1180,7 +1177,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); await store.findAll('person').then((records) => { assert.strictEqual(records.at(0), undefined); @@ -1217,7 +1214,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho this.owner.register('adapter:application', ApplicationAdapter); this.owner.register('serializer:application', class extends JSONAPISerializer {}); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [{ id: '1', type: 'person', attributes: { name: 'John' } }] }); @@ -1230,11 +1227,11 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); testInDebug('Calling adapterFor with a model class should assert', function (assert) { - let Person = Model.extend(); + const Person = Model.extend(); this.owner.register('model:person', Person); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { store.adapterFor(Person); diff --git a/tests/main/tests/unit/store/asserts-test.js b/tests/main/tests/unit/store/asserts-test.js index 8f3d14246f7..68eda1dc9f0 100644 --- a/tests/main/tests/unit/store/asserts-test.js +++ b/tests/main/tests/unit/store/asserts-test.js @@ -1,18 +1,18 @@ -import { run } from '@ember/runloop'; +import { settled } from '@ember/test-helpers'; import { module } from 'qunit'; +import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; import Model from '@ember-data/model'; -import Store from '@ember-data/store'; import test from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; -module('unit/store/asserts - DS.Store methods produce useful assertion messages', function (hooks) { +module('unit/store/asserts - Store methods produce useful assertion messages', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let { owner } = this; - owner.register('model:foo', Model.extend()); + const { owner } = this; + owner.register('model:foo', class extends Model {}); }); const MODEL_NAME_METHODS = [ @@ -32,7 +32,7 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' test('Calling Store methods with no modelName asserts', function (assert) { assert.expect(MODEL_NAME_METHODS.length); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); MODEL_NAME_METHODS.forEach((methodName) => { let assertion = `You need to pass a model name to the store's ${methodName} method`; @@ -50,7 +50,6 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'createRecord', 'deleteRecord', 'unloadRecord', - 'find', 'findRecord', 'getReference', 'peekRecord', @@ -68,11 +67,12 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'serializerFor', ]; - test('Calling Store methods after the store has been destroyed asserts', function (assert) { + test('Calling Store methods after the store has been destroyed asserts', async function (assert) { const store = new Store(); store.shouldAssertMethodCallsOnDestroyedStore = true; assert.expect(STORE_ENTRY_METHODS.length); - run(() => store.destroy()); + store.destroy(); + await settled(); STORE_ENTRY_METHODS.forEach((methodName) => { assert.expectAssertion(() => { diff --git a/tests/main/tests/unit/store/create-record-test.js b/tests/main/tests/unit/store/create-record-test.js index bdbc0487284..a09c9d7a5ec 100644 --- a/tests/main/tests/unit/store/create-record-test.js +++ b/tests/main/tests/unit/store/create-record-test.js @@ -7,6 +7,47 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; module('unit/store/createRecord - Store creating records', function (hooks) { setupTest(hooks); + test(`allows unknown properties to be delivered to the record (classic)`, function (assert) { + class Post extends Model { + @attr title; + recent = false; + } + const ClassicPost = Model.extend({ + title: attr(), + recent: false, + }); + + this.owner.register('model:post', Post); + this.owner.register('model:classic-post', ClassicPost); + const store = this.owner.lookup('service:store'); + + const originalInstantiate = store.instantiateRecord; + store.instantiateRecord = function (record, properties) { + assert.step('instantiateRecord'); + assert.strictEqual(properties.unknownProp, 'Unknown prop', 'unknownProp is passed along'); + assert.true(properties.recent, 'recent is passed along'); + return originalInstantiate.apply(this, arguments); + }; + + const record = store.createRecord('post', { + title: 'Ember.js is good', + recent: true, + unknownProp: 'Unknown prop', + }); + const classicRecord = store.createRecord('classic-post', { + title: 'Ember.js is good', + recent: true, + unknownProp: 'Unknown prop', + }); + + assert.strictEqual(record.unknownProp, 'Unknown prop', 'unknownProp is set'); + assert.true(record.recent, 'recent is set'); + assert.strictEqual(classicRecord.unknownProp, 'Unknown prop', 'unknownProp is set'); + assert.true(classicRecord.recent, 'recent is set'); + + assert.verifySteps(['instantiateRecord', 'instantiateRecord']); + }); + test(`doesn't modify passed in properties hash`, function (assert) { const Post = Model.extend({ title: attr(), @@ -28,8 +69,8 @@ module('unit/store/createRecord - Store creating records', function (hooks) { this.owner.register('model:comment', Comment); this.owner.register('model:author', Author); - let store = this.owner.lookup('service:store'); - let comment = store.push({ + const store = this.owner.lookup('service:store'); + const comment = store.push({ data: { type: 'comment', id: '1', @@ -38,7 +79,7 @@ module('unit/store/createRecord - Store creating records', function (hooks) { }, }, }); - let author = store.push({ + const author = store.push({ data: { type: 'author', id: '1', @@ -48,13 +89,13 @@ module('unit/store/createRecord - Store creating records', function (hooks) { }, }); - let properties = { + const properties = { title: 'My Post', randomProp: 'An unknown prop', comments: [comment], author, }; - let propertiesClone = { + const propertiesClone = { title: 'My Post', randomProp: 'An unknown prop', comments: [comment], @@ -79,7 +120,7 @@ module('unit/store/createRecord - Store creating records', function (hooks) { this.owner.register('model:record', Record); this.owner.register('model:storage', Storage); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); store.push({ data: [ @@ -100,8 +141,8 @@ module('unit/store/createRecord - Store creating records', function (hooks) { ], }); - let records = store.peekAll('record').slice(); - let storage = store.createRecord('storage', { name: 'Great store', records: records }); + const records = store.peekAll('record').slice(); + const storage = store.createRecord('storage', { name: 'Great store', records: records }); assert.strictEqual(storage.name, 'Great store', 'The attribute is well defined'); assert.strictEqual( @@ -127,9 +168,9 @@ module('unit/store/createRecord - Store with models by dash', function (hooks) { this.owner.register('model:some-thing', SomeThing); - let store = this.owner.lookup('service:store'); - let attributes = { foo: 'bar' }; - let record = store.createRecord('some-thing', attributes); + const store = this.owner.lookup('service:store'); + const attributes = { foo: 'bar' }; + const record = store.createRecord('some-thing', attributes); assert.strictEqual(record.foo, attributes.foo, 'The record is created'); assert.strictEqual(store.modelFor('some-thing').modelName, 'some-thing'); diff --git a/tests/main/tests/unit/store/finders-test.js b/tests/main/tests/unit/store/finders-test.js index 6a299fd51c6..ce37f161cda 100644 --- a/tests/main/tests/unit/store/finders-test.js +++ b/tests/main/tests/unit/store/finders-test.js @@ -1,10 +1,10 @@ import { module, test } from 'qunit'; -import { defer } from 'rsvp'; import { setupTest } 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'; class Person extends Model { @@ -35,7 +35,7 @@ module('unit/store/finders', function (hooks) { test('findRecord does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -45,7 +45,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'person') { serializerLoaded = true; @@ -53,7 +53,7 @@ module('unit/store/finders', function (hooks) { return serializerFor.call(this.store, modelName); }; - let storePromise = this.store.findRecord('person', 1); + const storePromise = this.store.findRecord('person', 1); assert.false(serializerLoaded, 'serializer is not eagerly loaded'); deferedFind.resolve({ @@ -68,7 +68,7 @@ module('unit/store/finders', function (hooks) { test('findMany does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -78,7 +78,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'person') { serializerLoaded = true; @@ -87,7 +87,7 @@ module('unit/store/finders', function (hooks) { }; this.store.findRecord('person', 1); - let storePromise = this.store.findRecord('person', 2); + const storePromise = this.store.findRecord('person', 2); assert.false(serializerLoaded, 'serializer is not eagerly loaded'); @@ -106,7 +106,7 @@ module('unit/store/finders', function (hooks) { test('findHasMany does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -116,7 +116,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'dog') { serializerLoaded = true; @@ -141,7 +141,7 @@ module('unit/store/finders', function (hooks) { }, }); - let storePromise = this.store.peekRecord('person', 1).dogs; + const storePromise = this.store.peekRecord('person', 1).dogs; assert.false(serializerLoaded, 'serializer is not eagerly loaded'); deferedFind.resolve({ @@ -159,7 +159,7 @@ module('unit/store/finders', function (hooks) { test('findBelongsTo does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -169,7 +169,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'dog') { serializerLoaded = true; @@ -194,7 +194,7 @@ module('unit/store/finders', function (hooks) { }, }); - let storePromise = this.store.peekRecord('person', 1).favoriteDog; + const storePromise = this.store.peekRecord('person', 1).favoriteDog; assert.false(serializerLoaded, 'serializer is not eagerly loaded'); @@ -208,7 +208,7 @@ module('unit/store/finders', function (hooks) { test('findAll does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -218,7 +218,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'person') { serializerLoaded = true; @@ -226,7 +226,7 @@ module('unit/store/finders', function (hooks) { return serializerFor.call(this.store, modelName); }; - let storePromise = this.store.findAll('person'); + const storePromise = this.store.findAll('person'); assert.false(serializerLoaded, 'serializer is not eagerly loaded'); deferedFind.resolve({ @@ -241,7 +241,7 @@ module('unit/store/finders', function (hooks) { test('query does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -251,7 +251,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'person') { serializerLoaded = true; @@ -259,7 +259,7 @@ module('unit/store/finders', function (hooks) { return serializerFor.call(this.store, modelName); }; - let storePromise = this.store.query('person', { first_duke_of_marlborough: true }); + const storePromise = this.store.query('person', { first_duke_of_marlborough: true }); assert.false(serializerLoaded, 'serializer is not eagerly loaded'); deferedFind.resolve({ @@ -274,7 +274,7 @@ module('unit/store/finders', function (hooks) { test('queryRecord does not load a serializer until the adapter promise resolves', async function (assert) { assert.expect(2); - let deferedFind = defer(); + const deferedFind = createDeferred(); this.owner.register( 'adapter:person', @@ -284,7 +284,7 @@ module('unit/store/finders', function (hooks) { ); let serializerLoaded = false; - let serializerFor = this.store.serializerFor; + const serializerFor = this.store.serializerFor; this.store.serializerFor = (modelName) => { if (modelName === 'person') { serializerLoaded = true; @@ -292,7 +292,7 @@ module('unit/store/finders', function (hooks) { return serializerFor.call(this.store, modelName); }; - let storePromise = this.store.queryRecord('person', { first_duke_of_marlborough: true }); + const storePromise = this.store.queryRecord('person', { first_duke_of_marlborough: true }); assert.false(serializerLoaded, 'serializer is not eagerly loaded'); deferedFind.resolve({ diff --git a/tests/main/tests/unit/store/model-for-test.js b/tests/main/tests/unit/store/model-for-test.js index 53bae338b56..55fa2beb193 100644 --- a/tests/main/tests/unit/store/model-for-test.js +++ b/tests/main/tests/unit/store/model-for-test.js @@ -1,10 +1,9 @@ -import { camelize, dasherize } from '@ember/string'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model from '@ember-data/model'; +import { camelize, dasherize } from '@ember-data/request-utils/string'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('unit/store/model_for - DS.Store#modelFor', function (hooks) { @@ -16,8 +15,8 @@ module('unit/store/model_for - DS.Store#modelFor', function (hooks) { }); test('when fetching factory from string, sets a normalized key as modelName', function (assert) { - let store = this.owner.lookup('service:store'); - let { __registry__: registry } = this.owner; + const store = this.owner.lookup('service:store'); + const { __registry__: registry } = this.owner; registry.normalize = (key) => dasherize(camelize(key)); @@ -26,8 +25,8 @@ module('unit/store/model_for - DS.Store#modelFor', function (hooks) { }); test('when fetching factory from string and dashing normalizer, sets a normalized key as modelName', function (assert) { - let store = this.owner.lookup('service:store'); - let { __registry__: registry } = this.owner; + const store = this.owner.lookup('service:store'); + const { __registry__: registry } = this.owner; registry.normalize = (key) => dasherize(camelize(key)); @@ -36,7 +35,7 @@ module('unit/store/model_for - DS.Store#modelFor', function (hooks) { }); testInDebug(`when fetching something that doesn't exist, throws error`, function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.throws(() => { store.modelFor('wild-stuff'); diff --git a/tests/main/tests/unit/store/peek-record-test.js b/tests/main/tests/unit/store/peek-record-test.js index bd784922b52..ab8aef32746 100644 --- a/tests/main/tests/unit/store/peek-record-test.js +++ b/tests/main/tests/unit/store/peek-record-test.js @@ -16,9 +16,9 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { }); test('peekRecord should return the record if it is in the store', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -32,9 +32,9 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { }); test('peekRecord should return the record with identifier as argument', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -48,7 +48,7 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { }); test('peekRecord should return null if the record is not in the store ', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.strictEqual( store.peekRecord('person', 1), @@ -58,7 +58,7 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { }); testInDebug('peekRecord should assert if not passed both model name and id', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { store.peekRecord('my-id'); @@ -66,10 +66,10 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { }); testInDebug('peekRecord should assert if passed a model class instead of model name', function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { - let modelClass = EmberObject.extend(); + const modelClass = EmberObject.extend(); store.peekRecord(modelClass, 'id'); }, /Passing classes to store methods has been removed/); }); @@ -90,7 +90,7 @@ module('unit/store/peekRecord - Store peekRecord', function (hooks) { { withType: true, withLid: true, extra: { id: null }, desc: 'type, null id, and lid' }, ].forEach(({ withType, withId, withLid, extra, isCreate, desc }) => { test(`peekRecord (${desc})`, function (assert) { - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); let person; if (isCreate) { diff --git a/tests/main/tests/unit/store/push-test.js b/tests/main/tests/unit/store/push-test.js index 486f066102a..42c64aa0643 100644 --- a/tests/main/tests/unit/store/push-test.js +++ b/tests/main/tests/unit/store/push-test.js @@ -1,8 +1,4 @@ -import EmberObject from '@ember/object'; -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -35,7 +31,7 @@ module('unit/store/push - Store#push', function (hooks) { test('Changed attributes are reset when matching data is pushed', function (assert) { const store = this.owner.lookup('service:store'); - let person = store.push({ + const person = store.push({ data: { type: 'person', id: '1', @@ -79,118 +75,111 @@ module('unit/store/push - Store#push', function (hooks) { assert.notOk(person.changedAttributes().firstName); }); - test('Calling push with a normalized hash returns a record', function (assert) { + test('Calling push with a normalized hash returns a record', async function (assert) { assert.expect(2); const store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - return run(() => { - let person = store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, - }, - }); - - return store.findRecord('person', 'wat').then((foundPerson) => { - assert.strictEqual( - foundPerson, - person, - 'record returned via load() is the same as the record returned from findRecord()' - ); - assert.deepEqual(foundPerson.getProperties('id', 'firstName', 'lastName'), { - id: 'wat', + const person = store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { firstName: 'Yehuda', lastName: 'Katz', - }); + }, + }, + }); + + await store.findRecord('person', 'wat').then((foundPerson) => { + assert.strictEqual( + foundPerson, + person, + 'record returned via push() is the same as the record returned from findRecord()' + ); + assert.deepEqual(foundPerson.getProperties('id', 'firstName', 'lastName'), { + id: 'wat', + firstName: 'Yehuda', + lastName: 'Katz', }); }); }); - test('Calling push with partial records updates just those attributes', function (assert) { + test('Calling push with partial records updates just those attributes', async function (assert) { assert.expect(2); const store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); adapter.shouldBackgroundReloadRecord = () => false; - return run(() => { - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - firstName: 'Yehuda', - lastName: 'Katz', - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', }, - }); + }, + }); - let person = store.peekRecord('person', 'wat'); + const person = store.peekRecord('person', 'wat'); - store.push({ - data: { - type: 'person', - id: 'wat', - attributes: { - lastName: 'Katz!', - }, + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + lastName: 'Katz!', }, - }); + }, + }); - return store.findRecord('person', 'wat').then((foundPerson) => { - assert.strictEqual( - foundPerson, - person, - 'record returned via load() is the same as the record returned from findRecord()' - ); - assert.deepEqual(foundPerson.getProperties('id', 'firstName', 'lastName'), { - id: 'wat', - firstName: 'Yehuda', - lastName: 'Katz!', - }); + await store.findRecord('person', 'wat').then((foundPerson) => { + assert.strictEqual( + foundPerson, + person, + 'record returned via load() is the same as the record returned from findRecord()' + ); + assert.deepEqual(foundPerson.getProperties('id', 'firstName', 'lastName'), { + id: 'wat', + firstName: 'Yehuda', + lastName: 'Katz!', }); }); }); test('Calling push on normalize allows partial updates with raw JSON', function (assert) { this.owner.register('serializer:person', RESTSerializer); - let person; const store = this.owner.lookup('service:store'); - run(() => { - person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Robert', - lastName: 'Jackson', - }, + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Robert', + lastName: 'Jackson', }, - }); - - store.push( - store.normalize('person', { - id: '1', - firstName: 'Jacquie', - }) - ); + }, }); + store.push( + store.normalize('person', { + id: '1', + firstName: 'Jacquie', + }) + ); + assert.strictEqual(person.firstName, 'Jacquie', 'you can push raw JSON into the store'); assert.strictEqual(person.lastName, 'Jackson', 'existing fields are untouched'); }); - test('Calling push with a normalized hash containing IDs of related records returns a record', function (assert) { + test('Calling push with a normalized hash containing IDs of related records returns a record', async function (assert) { assert.expect(1); const store = this.owner.lookup('service:store'); @@ -202,11 +191,11 @@ module('unit/store/push - Store#push', function (hooks) { } this.owner.register('model:person', Person); - let adapter = store.adapterFor('application'); + const adapter = store.adapterFor('application'); adapter.findRecord = function (store, type, id) { if (id === '1') { - return resolve({ + return Promise.resolve({ data: { id: '1', type: 'phone-number', @@ -221,7 +210,7 @@ module('unit/store/push - Store#push', function (hooks) { } if (id === '2') { - return resolve({ + return Promise.resolve({ data: { id: '2', type: 'phone-number', @@ -236,77 +225,75 @@ module('unit/store/push - Store#push', function (hooks) { } }; - return run(() => { - let normalized = store.normalize('person', { - id: 'wat', - type: 'person', - attributes: { - 'first-name': 'John', - 'last-name': 'Smith', - }, - relationships: { - 'phone-numbers': { - data: [ - { id: '1', type: 'phone-number' }, - { id: '2', type: 'phone-number' }, - ], - }, + const normalized = store.normalize('person', { + id: 'wat', + type: 'person', + attributes: { + 'first-name': 'John', + 'last-name': 'Smith', + }, + relationships: { + 'phone-numbers': { + data: [ + { id: '1', type: 'phone-number' }, + { id: '2', type: 'phone-number' }, + ], }, - }); - let person = store.push(normalized); + }, + }); + const person = store.push(normalized); - return person.phoneNumbers.then((phoneNumbers) => { - let items = phoneNumbers.map((item) => { - return item ? item.getProperties('id', 'number', 'person') : null; - }); - assert.deepEqual(items, [ - { - id: '1', - number: '5551212', - person: person, - }, - { - id: '2', - number: '5552121', - person: person, - }, - ]); + await person.phoneNumbers.then((phoneNumbers) => { + const items = phoneNumbers.map((item) => { + return item ? item.getProperties('id', 'number', 'person') : null; }); + assert.deepEqual(items, [ + { + id: '1', + number: '5551212', + person: person, + }, + { + id: '2', + number: '5552121', + person: person, + }, + ]); }); }); testInDebug('calling push without data argument as an object raises an error', function (assert) { const store = this.owner.lookup('service:store'); - let invalidValues = [null, 1, 'string', EmberObject.create(), EmberObject.extend(), true]; + const invalidValues = [null, 1, 'string', class {}, true]; - assert.expect(invalidValues.length); + assert.expect(invalidValues.length + 1); invalidValues.forEach((invalidValue) => { assert.expectAssertion(() => { - run(() => { - store.push('person', invalidValue); - }); - }, /object/); + store.push(invalidValue); + }, /Expected a JSON:API Document as the content provided to the cache/); }); + + assert.expectAssertion(() => { + store.push({}); + }, /Expected a resource object in the 'data' property in the document provided to the cache/); }); testInDebug('Calling push with a link for a non async relationship should warn if no data', function (assert) { const store = this.owner.lookup('service:store'); assert.expectWarning(() => { - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - phoneNumbers: { - links: { - related: '/api/people/1/phone-numbers', - }, + store.push({ + data: { + type: 'person', + id: '1', + relationships: { + phoneNumbers: { + links: { + related: '/api/people/1/phone-numbers', }, }, }, - }); + }, }); }, /You pushed a record of type 'person' with a relationship 'phoneNumbers' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty./); }); @@ -316,24 +303,22 @@ module('unit/store/push - Store#push', function (hooks) { function (assert) { const store = this.owner.lookup('service:store'); assert.expectNoWarning(() => { - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - phoneNumbers: { - data: [ - { type: 'phone-number', id: '2' }, - { type: 'phone-number', id: '3' }, - ], - links: { - related: '/api/people/1/phone-numbers', - }, + store.push({ + data: { + type: 'person', + id: '1', + relationships: { + phoneNumbers: { + data: [ + { type: 'phone-number', id: '2' }, + { type: 'phone-number', id: '3' }, + ], + links: { + related: '/api/people/1/phone-numbers', }, }, }, - }); + }, }); }); } @@ -368,7 +353,7 @@ module('unit/store/push - Store#push', function (hooks) { ], }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.strictEqual(person.phoneNumbers.length, 1); assert.strictEqual(person.phoneNumbers.at(0).number, '1-800-DATA'); @@ -398,15 +383,13 @@ module('unit/store/push - Store#push', function (hooks) { testInDebug('Calling push with an unknown model name throws an assertion error', function (assert) { const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { - run(() => { - store.push({ - data: { - id: '1', - type: 'unknown', - }, - }); + store.push({ + data: { + id: '1', + type: 'unknown', + }, }); - }, /You tried to push data with a type 'unknown' but no model could be found with that name/); + }, "Missing Resource Type: received resource data with a type 'unknown' but no schema could be found with that name."); }); test('Calling push with a link containing an object', function (assert) { @@ -419,50 +402,46 @@ module('unit/store/push - Store#push', function (hooks) { this.owner.register('model:person', Person); const store = this.owner.lookup('service:store'); - run(() => { - store.push( - store.normalize('person', { - id: '1', - type: 'person', - attributes: { - 'first-name': 'Tan', - }, - relationships: { - 'phone-numbers': { - links: { related: '/api/people/1/phone-numbers' }, - }, + store.push( + store.normalize('person', { + id: '1', + type: 'person', + attributes: { + 'first-name': 'Tan', + }, + relationships: { + 'phone-numbers': { + links: { related: '/api/people/1/phone-numbers' }, }, - }) - ); - }); + }, + }) + ); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.strictEqual(person.firstName, 'Tan', 'you can use links containing an object'); }); test('Calling push with a link containing the value null', function (assert) { const store = this.owner.lookup('service:store'); - run(() => { - store.push( - store.normalize('person', { - id: '1', - type: 'person', - attributes: { - 'first-name': 'Tan', - }, - relationships: { - 'phone-numbers': { - links: { - related: null, - }, + store.push( + store.normalize('person', { + id: '1', + type: 'person', + attributes: { + 'first-name': 'Tan', + }, + relationships: { + 'phone-numbers': { + links: { + related: null, }, }, - }) - ); - }); + }, + }) + ); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.strictEqual(person.firstName, 'Tan', 'you can use links that contain null as a value'); }); @@ -470,34 +449,32 @@ module('unit/store/push - Store#push', function (hooks) { testInDebug('calling push with hasMany relationship the value must be an array', function (assert) { const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - phoneNumbers: { - data: 1, - }, + store.push({ + data: { + type: 'person', + id: '1', + relationships: { + phoneNumbers: { + data: 1, }, }, - }); + }, }); }); }); testInDebug('calling push with missing or invalid `id` throws assertion error', function (assert) { + this.owner.register('model:user', class extends Model {}); const store = this.owner.lookup('service:store'); - let invalidValues = [{}, { id: null }, { id: '' }]; + + const invalidValues = [{ type: 'user' }, { id: null, type: 'user' }, { id: '', type: 'user' }]; assert.expect(invalidValues.length); invalidValues.forEach((invalidValue) => { assert.expectAssertion(() => { - run(() => { - store.push({ - data: invalidValue, - }); + store.push({ + data: invalidValue, }); }, /You must include an 'id'/); }); @@ -506,18 +483,16 @@ module('unit/store/push - Store#push', function (hooks) { testInDebug('calling push with belongsTo relationship the value must not be an array', function (assert) { const store = this.owner.lookup('service:store'); assert.expectAssertion(() => { - run(() => { - store.push({ - data: { - type: 'phone-number', - id: '1', - relationships: { - person: { - data: [1], - }, + store.push({ + data: { + type: 'phone-number', + id: '1', + relationships: { + person: { + data: [1], }, }, - }); + }, }); }, /must not be an array/); }); @@ -525,25 +500,23 @@ module('unit/store/push - Store#push', function (hooks) { testInDebug('Calling push with unknown keys should not warn by default', function (assert) { const store = this.owner.lookup('service:store'); assert.expectNoWarning(() => { - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Tomster', - emailAddress: 'tomster@emberjs.com', - isMascot: true, - }, + store.push({ + data: { + type: 'person', + id: '1', + attributes: { + firstName: 'Tomster', + emailAddress: 'tomster@emberjs.com', + isMascot: true, }, - }); + }, }); }, /The payload for 'person' contains these unknown .*: .* Make sure they've been defined in your model./); }); test('_push returns an identifier if an object is pushed', function (assert) { const store = this.owner.lookup('service:store'); - let pushResult = store._push({ + const pushResult = store._push({ data: { id: '1', type: 'person', @@ -556,18 +529,16 @@ module('unit/store/push - Store#push', function (hooks) { test('_push does not require a modelName to resolve to a modelClass', function (assert) { const store = this.owner.lookup('service:store'); - let originalCall = store.modelFor; + const originalCall = store.modelFor; store.modelFor = function () { assert.notOk('modelFor was triggered as a result of a call to store._push'); }; - run(() => { - store._push({ - data: { - id: '1', - type: 'person', - }, - }); + store._push({ + data: { + id: '1', + type: 'person', + }, }); store.modelFor = originalCall; @@ -576,17 +547,13 @@ module('unit/store/push - Store#push', function (hooks) { test('_push returns an array of identifiers if an array is pushed', function (assert) { const store = this.owner.lookup('service:store'); - let pushResult; - - run(() => { - pushResult = store._push({ - data: [ - { - id: '1', - type: 'person', - }, - ], - }); + const pushResult = store._push({ + data: [ + { + id: '1', + type: 'person', + }, + ], }); assert.ok(pushResult instanceof Array); @@ -596,12 +563,8 @@ module('unit/store/push - Store#push', function (hooks) { test('_push returns null if no data is pushed', function (assert) { const store = this.owner.lookup('service:store'); - let pushResult; - - run(() => { - pushResult = store._push({ - data: null, - }); + const pushResult = store._push({ + data: null, }); assert.strictEqual(pushResult, null); @@ -620,30 +583,26 @@ module('unit/store/push - Store#pushPayload', function (hooks) { test('Calling pushPayload allows pushing raw JSON', function (assert) { const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload('post', { - posts: [ - { - id: '1', - postTitle: 'Ember rocks', - }, - ], - }); + store.pushPayload('post', { + posts: [ + { + id: '1', + postTitle: 'Ember rocks', + }, + ], }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); assert.strictEqual(post.postTitle, 'Ember rocks', 'you can push raw JSON into the store'); - run(() => { - store.pushPayload('post', { - posts: [ - { - id: '1', - postTitle: 'Ember rocks (updated)', - }, - ], - }); + store.pushPayload('post', { + posts: [ + { + id: '1', + postTitle: 'Ember rocks (updated)', + }, + ], }); assert.strictEqual(post.postTitle, 'Ember rocks (updated)', 'You can update data in the store'); @@ -652,26 +611,22 @@ module('unit/store/push - Store#pushPayload', function (hooks) { test('Calling pushPayload allows pushing singular payload properties', function (assert) { const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload('post', { - post: { - id: '1', - postTitle: 'Ember rocks', - }, - }); + store.pushPayload('post', { + post: { + id: '1', + postTitle: 'Ember rocks', + }, }); - let post = store.peekRecord('post', 1); + const post = store.peekRecord('post', 1); assert.strictEqual(post.postTitle, 'Ember rocks', 'you can push raw JSON into the store'); - run(() => { - store.pushPayload('post', { - post: { - id: '1', - postTitle: 'Ember rocks (updated)', - }, - }); + store.pushPayload('post', { + post: { + id: '1', + postTitle: 'Ember rocks (updated)', + }, }); assert.strictEqual(post.postTitle, 'Ember rocks (updated)', 'You can update data in the store'); @@ -707,28 +662,26 @@ module('unit/store/push - Store#pushPayload', function (hooks) { const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload('post', { - posts: [ - { - id: '1', - postTitle: 'Ember rocks', - }, - ], - people: [ - { - id: '2', - firstName: 'Yehuda', - }, - ], - }); + store.pushPayload('post', { + posts: [ + { + id: '1', + postTitle: 'Ember rocks', + }, + ], + people: [ + { + id: '2', + firstName: 'Yehuda', + }, + ], }); - let post = store.peekRecord('post', '1'); + const post = store.peekRecord('post', '1'); assert.strictEqual(post.postTitle, 'Ember rocks', 'you can push raw JSON into the store'); - let person = store.peekRecord('person', '2'); + const person = store.peekRecord('person', '2'); assert.strictEqual(person.firstName, 'Yehuda', 'you can push raw JSON into the store'); }); @@ -747,10 +700,8 @@ module('unit/store/push - Store#pushPayload', function (hooks) { ); const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload({ - posts: [{ id: '1', postTitle: 'Ember rocks' }], - }); + store.pushPayload({ + posts: [{ id: '1', postTitle: 'Ember rocks' }], }); }); @@ -784,21 +735,19 @@ module('unit/store/push - Store#pushPayload', function (hooks) { ); const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload({ - posts: [ - { - id: '1', - postTitle: 'Ember rocks', - }, - ], - people: [ - { - id: '2', - firstName: 'Yehuda', - }, - ], - }); + store.pushPayload({ + posts: [ + { + id: '1', + postTitle: 'Ember rocks', + }, + ], + people: [ + { + id: '2', + firstName: 'Yehuda', + }, + ], }); var post = store.peekRecord('post', 1); @@ -821,32 +770,28 @@ module('unit/store/push - Store#pushPayload', function (hooks) { ); const store = this.owner.lookup('service:store'); - run(() => { - store.pushPayload('person', { - people: [ - { - id: '1', - firstName: 'Robert', - lastName: 'Jackson', - }, - ], - }); + store.pushPayload('person', { + people: [ + { + id: '1', + firstName: 'Robert', + lastName: 'Jackson', + }, + ], }); - let person = store.peekRecord('person', 1); + const person = store.peekRecord('person', 1); assert.strictEqual(person.firstName, 'Robert', 'you can push raw JSON into the store'); assert.strictEqual(person.lastName, 'Jackson', 'you can push raw JSON into the store'); - run(() => { - store.pushPayload('person', { - people: [ - { - id: '1', - firstName: 'Jacquie', - }, - ], - }); + store.pushPayload('person', { + people: [ + { + id: '1', + firstName: 'Jacquie', + }, + ], }); assert.strictEqual(person.firstName, 'Jacquie', 'you can push raw JSON into the store'); @@ -922,7 +867,7 @@ module('unit/store/push - Store#pushPayload', function (hooks) { ], }); - let robert = store.peekRecord('person', '1'); + const robert = store.peekRecord('person', '1'); const friends = robert.friends; assert.strictEqual(friends.at(0).id, '2', 'first object is unchanged'); @@ -957,31 +902,29 @@ module('unit/store/push - Store#push with JSON-API', function (hooks) { assert.expect(2); const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Tom Dale', - }, + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', }, - { - type: 'person', - id: '2', - attributes: { - name: 'Tomster', - }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Tomster', }, - ], - }); + }, + ], }); - let tom = store.peekRecord('person', 1); + const tom = store.peekRecord('person', 1); assert.strictEqual(tom.name, 'Tom Dale', 'Tom should be in the store'); - let tomster = store.peekRecord('person', 2); + const tomster = store.peekRecord('person', 2); assert.strictEqual(tomster.name, 'Tomster', 'Tomster should be in the store'); }); @@ -989,52 +932,50 @@ module('unit/store/push - Store#push with JSON-API', function (hooks) { assert.expect(2); const store = this.owner.lookup('service:store'); - run(() => { - store.push({ - data: [ - { - type: 'person', - id: '1', - attributes: { - name: 'Tomster', - }, - relationships: { - cars: [ - { - data: { - type: 'person', - id: '1', - }, - }, - ], - }, + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Tomster', }, - ], - included: [ - { - type: 'car', - id: '1', - attributes: { - make: 'Dodge', - model: 'Neon', - }, - relationships: { - person: { + relationships: { + cars: [ + { data: { - id: '1', type: 'person', + id: '1', }, }, + ], + }, + }, + ], + included: [ + { + type: 'car', + id: '1', + attributes: { + make: 'Dodge', + model: 'Neon', + }, + relationships: { + person: { + data: { + id: '1', + type: 'person', + }, }, }, - ], - }); + }, + ], }); - let tomster = store.peekRecord('person', 1); + const tomster = store.peekRecord('person', 1); assert.strictEqual(tomster.name, 'Tomster', 'Tomster should be in the store'); - let car = store.peekRecord('car', 1); + const car = store.peekRecord('car', 1); assert.strictEqual(car.model, 'Neon', "Tomster's car should be in the store"); }); }); diff --git a/tests/main/tests/unit/store/unload-test.js b/tests/main/tests/unit/store/unload-test.js index 0b12c1c8a41..98dc25ecbe3 100644 --- a/tests/main/tests/unit/store/unload-test.js +++ b/tests/main/tests/unit/store/unload-test.js @@ -1,8 +1,6 @@ import { get } from '@ember/object'; -import { run } from '@ember/runloop'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -18,7 +16,7 @@ module('unit/store/unload - Store unloading records', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { - let Record = Model.extend({ + const Record = Model.extend({ title: attr('string'), wasFetched: attr('boolean'), }); @@ -31,7 +29,7 @@ module('unit/store/unload - Store unloading records', function (hooks) { Adapter.extend({ findRecord(store, type, id, snapshot) { tryToFind = true; - return resolve({ + return Promise.resolve({ data: { id, type: snapshot.modelName, attributes: { 'was-fetched': true } }, }); }, @@ -44,7 +42,7 @@ module('unit/store/unload - Store unloading records', function (hooks) { testInDebug('unload an in-flight record asserts', async function (assert) { assert.expect(2); - let record = store.push({ + const record = store.push({ data: { type: 'record', id: '1', @@ -65,7 +63,7 @@ module('unit/store/unload - Store unloading records', function (hooks) { record.set('title', 'toto2'); assert.strictEqual(get(record, 'hasDirtyAttributes'), true, 'record is dirty'); - let promise = record.save(); + const promise = record.save(); assert.expectAssertion( function () { @@ -80,51 +78,45 @@ module('unit/store/unload - Store unloading records', function (hooks) { await promise; }); - test('unload a record', function (assert) { + test('unload a record', async function (assert) { assert.expect(2); - return run(() => { - store.push({ - data: { - type: 'record', - id: '1', - attributes: { - title: 'toto', - }, + store.push({ + data: { + type: 'record', + id: '1', + attributes: { + title: 'toto', }, - }); + }, + }); - return store.findRecord('record', 1).then((record) => { - assert.strictEqual(get(record, 'id'), '1', 'found record with id 1'); + const record = await store.findRecord('record', '1'); + assert.strictEqual(get(record, 'id'), '1', 'found record with id 1'); - run(() => store.unloadRecord(record)); + store.unloadRecord(record); - tryToFind = false; + tryToFind = false; - return store.findRecord('record', 1).then(() => { - assert.true(tryToFind, 'not found record with id 1'); - }); - }); - }); + await store.findRecord('record', '1'); + assert.true(tryToFind, 'not found record with id 1'); }); - test('unload followed by create of the same type + id', function (assert) { - let record = store.createRecord('record', { id: '1' }); + test('unload followed by create of the same type + id', async function (assert) { + const record = store.createRecord('record', { id: '1' }); assert.strictEqual(store.peekRecord('record', 1), record, 'record should exactly equal'); - return run(() => { - record.unloadRecord(); - let createdRecord = store.createRecord('record', { id: '1' }); - assert.notStrictEqual(record, createdRecord, 'newly created record is fresh (and was created)'); - }); + record.unloadRecord(); + const createdRecord = store.createRecord('record', { id: '1' }); + assert.notStrictEqual(record, createdRecord, 'newly created record is fresh (and was created)'); }); }); module('Store - unload record with relationships', function (hooks) { setupTest(hooks); - test('can commit store after unload record with relationships', function (assert) { + test('can commit store after unload record with relationships', async function (assert) { assert.expect(1); const Brand = Model.extend({ @@ -155,8 +147,11 @@ module('Store - unload record with relationships', function (hooks) { this.owner.register( 'adapter:application', Adapter.extend({ + shouldBackgroundReloadRecord() { + return false; + }, findRecord(store, type, id, snapshot) { - return resolve({ + return Promise.resolve({ data: { id: '1', type: snapshot.modelName, @@ -169,56 +164,51 @@ module('Store - unload record with relationships', function (hooks) { }, createRecord(store, type, snapshot) { - return resolve(); + return Promise.resolve(); }, }) ); - let store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store'); - return run(() => { - store.push({ - data: [ - { - type: 'brand', - id: '1', - attributes: { - name: 'EmberJS', - }, + store.push({ + data: [ + { + type: 'brand', + id: '1', + attributes: { + name: 'EmberJS', }, - { - type: 'product', - id: '1', - attributes: { - description: 'toto', - }, - relationships: { - brand: { - data: { type: 'brand', id: '1' }, - }, + }, + { + type: 'product', + id: '1', + attributes: { + description: 'toto', + }, + relationships: { + brand: { + data: { type: 'brand', id: '1' }, }, }, - ], - }); + }, + ], + }); - let product = store.peekRecord('product', 1); - let like = store.createRecord('like', { id: '1', product: product }); + let product = store.peekRecord('product', 1); + const like = store.createRecord('like', { id: '1', product: product }); - return like.save(); - }) - .then(() => { - // TODO: this is strange, future travelers please address - run(() => store.unloadRecord(store.peekRecord('product', 1))); - }) - .then(() => { - return store.findRecord('product', 1); - }) - .then((product) => { - assert.strictEqual( - product.description, - 'cuisinart', - "The record was unloaded and the adapter's `findRecord` was called" - ); - }); + await like.save(); + + // TODO: this is strange, future travelers please address + store.unloadRecord(product); + + product = await store.findRecord('product', '1'); + + assert.strictEqual( + product.description, + 'cuisinart', + "The record was unloaded and the adapter's `findRecord` was called" + ); }); }); diff --git a/tests/main/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js b/tests/main/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js index 9c52cf1efa5..bd2a7860989 100644 --- a/tests/main/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js +++ b/tests/main/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js @@ -1,11 +1,8 @@ -import { run } from '@ember/runloop'; - import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; -import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; module('unit/relationships/relationship-payloads-manager (polymorphic)', function (hooks) { @@ -33,7 +30,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio let id = 1; function makeHat(type, props) { - const resource = deepCopy(props); + const resource = structuredClone(props); resource.id = `${id++}`; resource.type = type; resource.attributes.type = type; @@ -62,7 +59,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio included: [hatData1, bigHatData1, smallHatData1], }; - const user = run(() => this.store.push(userData)); + const user = this.store.push(userData); const finalResult = user.hats.map((r) => r.type); @@ -70,11 +67,11 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('push one side is polymorphic, subType then baseType', function (assert) { - let User = Model.extend({ + const User = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, inverse: 'user' }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), user: belongsTo('user', { async: false, inverse: 'hats', as: 'hat' }), }); @@ -87,7 +84,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio let id = 1; function makeHat(type, props) { - const resource = deepCopy(props); + const resource = structuredClone(props); resource.id = `${id++}`; resource.type = type; resource.attributes.type = type; @@ -117,7 +114,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio included, }; - const user = run(() => this.store.push(userData)), + const user = this.store.push(userData), finalResult = user.hats.map((r) => r.type), expectedResults = included.map((m) => m.type); @@ -125,11 +122,11 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('push one side is polymorphic, different subtypes', function (assert) { - let User = Model.extend({ + const User = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, inverse: 'user' }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), user: belongsTo('user', { async: false, inverse: 'hats', as: 'hat' }), }); @@ -142,7 +139,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio let id = 1; function makeHat(type, props) { - const resource = deepCopy(props); + const resource = structuredClone(props); resource.id = `${id++}`; resource.type = type; resource.attributes.type = type; @@ -173,7 +170,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio included, }; - const user = run(() => this.store.push(userData)), + const user = this.store.push(userData), finalResult = user.hats.map((r) => r.type), expectedResults = included.map((m) => m.type); @@ -181,11 +178,11 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('push both sides are polymorphic', function (assert) { - let User = Model.extend({ + const User = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, as: 'user', inverse: 'user' }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), user: belongsTo('user', { async: false, inverse: 'hats', polymorphic: true, as: 'hat' }), }); @@ -199,7 +196,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio let id = 1; function makeHat(type, props) { - const resource = deepCopy(props); + const resource = structuredClone(props); resource.id = `${id++}`; resource.type = type; resource.attributes.type = type; @@ -229,14 +226,14 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }; const expectedAlienResults = alienIncluded.map((m) => m.type), - alien = run(() => this.store.push(alienData)), + alien = this.store.push(alienData), alienFinalHats = alien.hats.map((r) => r.type); assert.deepEqual(alienFinalHats, expectedAlienResults, 'We got all alien hats!'); }); test('handles relationships where both sides are polymorphic', function (assert) { - let Person = Model.extend({ + const Person = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, @@ -245,7 +242,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), person: belongsTo('person', { async: false, @@ -303,13 +300,8 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio included: [bigHatData3, smallHatData3], }; - const bigPerson = run(() => { - return this.store.push(bigPersonData); - }); - - const smallPerson = run(() => { - return this.store.push(smallPersonData); - }); + const bigPerson = this.store.push(bigPersonData); + const smallPerson = this.store.push(smallPersonData); const finalBigResult = bigPerson.hats.slice(); const finalSmallResult = smallPerson.hats.slice(); @@ -325,7 +317,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio if (Array.isArray(b)) { rel.data = b.map((i) => { - let { type, id } = i; + const { type, id } = i; if (recurse === true) { link(i, [a], relationshipName, false); @@ -345,7 +337,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio } } - let Person = Model.extend({ + const Person = Model.extend({ name: attr(), family: hasMany('person', { async: false, polymorphic: true, inverse: 'family', as: 'person' }), twin: belongsTo('person', { async: false, polymorphic: true, inverse: 'twin', as: 'person' }), @@ -400,10 +392,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio { type: 'grownup', id: motherPayload.id }, ]; const expectedTwinReference = { type: 'girl', id: sisterPayload.id }; - - const boyInstance = run(() => { - return this.store.push(payload); - }); + const boyInstance = this.store.push(payload); const familyResultReferences = boyInstance.family.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; @@ -422,7 +411,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio if (Array.isArray(b)) { rel.data = b.map((i) => { - let { type, id } = i; + const { type, id } = i; if (recurse === true) { link(i, [a], relationshipName, false); @@ -500,10 +489,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio { type: 'grownup', id: motherPayload.id }, ]; const expectedTwinReference = { type: 'girl', id: sisterPayload.id }; - - const boyInstance = run(() => { - return this.store.push(payload); - }); + const boyInstance = this.store.push(payload); const familyResultReferences = boyInstance.family.slice().map((i) => { return { type: i.constructor.modelName, id: i.id }; @@ -568,7 +554,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('push polymorphic self-referential circular non-reflexive relationship', function (assert) { - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), hat: belongsTo('hat', { async: false, inverse: 'hats', polymorphic: true, as: 'hat' }), hats: hasMany('hat', { async: false, inverse: 'hat', polymorphic: true, as: 'hat' }), @@ -593,7 +579,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }, }; - const hat = run(() => this.store.push(hatData)); + const hat = this.store.push(hatData); const expectedHatReference = { id: '1', type: 'big-hat' }; const expectedHatsReferences = [{ id: '1', type: 'big-hat' }]; @@ -612,49 +598,47 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('polymorphic hasMany to types with separate id-spaces', function (assert) { - let User = Model.extend({ + const User = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, inverse: 'user', as: 'user' }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), user: belongsTo('user', { async: false, inverse: 'hats', polymorphic: true, as: 'hat' }), }); - let BigHat = Hat.extend({}); - let SmallHat = Hat.extend({}); + const BigHat = Hat.extend({}); + const SmallHat = Hat.extend({}); this.owner.register('model:user', User); this.owner.register('model:hat', Hat); this.owner.register('model:big-hat', BigHat); this.owner.register('model:small-hat', SmallHat); - const user = run(() => - this.store.push({ - data: { - id: '1', - type: 'user', - relationships: { - hats: { - data: [ - { id: '1', type: 'big-hat' }, - { id: '1', type: 'small-hat' }, - ], - }, + const user = this.store.push({ + data: { + id: '1', + type: 'user', + relationships: { + hats: { + data: [ + { id: '1', type: 'big-hat' }, + { id: '1', type: 'small-hat' }, + ], }, }, - included: [ - { - id: '1', - type: 'big-hat', - }, - { - id: '1', - type: 'small-hat', - }, - ], - }) - ); + }, + included: [ + { + id: '1', + type: 'big-hat', + }, + { + id: '1', + type: 'small-hat', + }, + ], + }); const hats = user.hats; @@ -669,11 +653,11 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('polymorphic hasMany to types with separate id-spaces, from inverse payload', function (assert) { - let User = Model.extend({ + const User = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, inverse: 'user', as: 'user' }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), user: belongsTo('user', { async: false, inverse: 'hats', polymorphic: true, as: 'hat' }), }); @@ -683,34 +667,32 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio this.owner.register('model:big-hat', Hat.extend({})); this.owner.register('model:small-hat', Hat.extend({})); - const user = run(() => - this.store.push({ - data: { + const user = this.store.push({ + data: { + id: '1', + type: 'user', + }, + included: [ + { id: '1', - type: 'user', - }, - included: [ - { - id: '1', - type: 'big-hat', - relationships: { - user: { - data: { id: '1', type: 'user' }, - }, + type: 'big-hat', + relationships: { + user: { + data: { id: '1', type: 'user' }, }, }, - { - id: '1', - type: 'small-hat', - relationships: { - user: { - data: { id: '1', type: 'user' }, - }, + }, + { + id: '1', + type: 'small-hat', + relationships: { + user: { + data: { id: '1', type: 'user' }, }, }, - ], - }) - ); + }, + ], + }); const hats = user.hats; @@ -725,7 +707,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }); test('polymorphic hasMany to polymorphic hasMany types with separate id-spaces', function (assert) { - let Person = Model.extend({ + const Person = Model.extend({ hats: hasMany('hat', { async: false, polymorphic: true, @@ -734,7 +716,7 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }), }); - let Hat = Model.extend({ + const Hat = Model.extend({ type: attr('string'), person: belongsTo('person', { async: false, @@ -794,13 +776,8 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio included: [bigHatData3, smallHatData3], }; - const bigPerson = run(() => { - return this.store.push(bigPersonData); - }); - - const smallPerson = run(() => { - return this.store.push(smallPersonData); - }); + const bigPerson = this.store.push(bigPersonData); + const smallPerson = this.store.push(smallPersonData); const finalBigResult = bigPerson.hats.slice(); const finalSmallResult = smallPerson.hats.slice(); @@ -841,33 +818,31 @@ module('unit/relationships/relationship-payloads-manager (polymorphic)', functio }) ); - let runInvalidPush = () => { - return run(() => { - return this.store.push({ - data: { - type: 'post', - id: '1', - relationships: { - comments: { - data: [{ type: 'comment', id: '1' }], - }, + const runInvalidPush = () => { + return this.store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + data: [{ type: 'comment', id: '1' }], }, }, - included: [ - { - type: 'comment', - id: '1', - relationships: { - post: { - data: { - type: 'post', - id: '1', - }, + }, + included: [ + { + type: 'comment', + id: '1', + relationships: { + post: { + data: { + type: 'post', + id: '1', }, }, }, - ], - }); + }, + ], }); }; 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 b1d3e344440..0ca3f94b149 100644 --- a/tests/main/tests/unit/system/snapshot-record-array-test.js +++ b/tests/main/tests/unit/system/snapshot-record-array-test.js @@ -12,14 +12,14 @@ module('Unit - snapshot-record-array', function (hooks) { setupTest(hooks); test('constructor', function (assert) { - let array = A([1, 2]); + const array = A([1, 2]); array.content = [1, 2]; - let options = { + const options = { adapterOptions: 'some options', include: 'include me', }; - let snapshot = new SnapshotRecordArray( + const snapshot = new SnapshotRecordArray( { peekAll() { return array; @@ -53,21 +53,21 @@ module('Unit - snapshot-record-array', function (hooks) { }, }); - let options = { + const options = { adapterOptions: 'some options', include: 'include me', }; let didTakeSnapshot = 0; - let snapshotsTaken = []; + const snapshotsTaken = []; const create = store._fetchManager.createSnapshot; store._fetchManager.createSnapshot = function () { didTakeSnapshot++; - let snapshot = create.apply(this, arguments); + const snapshot = create.apply(this, arguments); snapshotsTaken.push(snapshot); return snapshot; }; - let snapshot = new SnapshotRecordArray(store, 'dog', options); + const snapshot = new SnapshotRecordArray(store, 'dog', options); assert.strictEqual(didTakeSnapshot, 0, 'no shapshot should yet be taken'); assert.strictEqual(snapshot.snapshots()[0], snapshotsTaken[0], 'should be correct snapshot'); @@ -84,7 +84,7 @@ module('Unit - snapshot-record-array', function (hooks) { until: '5.0', }, function (assert) { - let array = A([1, 2]); + const array = A([1, 2]); let typeLoaded = false; Object.defineProperty(array, 'type', { @@ -94,12 +94,12 @@ module('Unit - snapshot-record-array', function (hooks) { }, }); - let options = { + const options = { adapterOptions: 'some options', include: 'include me', }; - let snapshot = new SnapshotRecordArray( + const snapshot = new SnapshotRecordArray( { peekAll() { return array; diff --git a/tests/main/tests/unit/utils/determine-body-promise-test.js b/tests/main/tests/unit/utils/determine-body-promise-test.js index 030cf52b36a..d377ab7a313 100644 --- a/tests/main/tests/unit/utils/determine-body-promise-test.js +++ b/tests/main/tests/unit/utils/determine-body-promise-test.js @@ -1,7 +1,6 @@ // Tests copied from ember-fetch addon import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { determineBodyPromise } from '@ember-data/adapter/-private'; @@ -14,7 +13,7 @@ class Response { } text() { - return resolve(this._text); + return Promise.resolve(this._text); } } @@ -74,7 +73,7 @@ module('Unit | determineBodyPromise', function () { }); }); - test('determineBodyResponse returns undefined when the http status code is 204', function (assert) { + test('determineBodyResponse returns undefined when the http status code is 204 and response is "null"', function (assert) { assert.expect(1); const response = new Response('null', { status: 204 }); @@ -85,7 +84,7 @@ module('Unit | determineBodyPromise', function () { }); }); - test('determineBodyResponse returns undefined when the http status code is 205', function (assert) { + test('determineBodyResponse returns undefined when the http status code is 205 and response is "null"', function (assert) { assert.expect(1); const response = new Response('null', { status: 205 }); @@ -96,7 +95,7 @@ module('Unit | determineBodyPromise', function () { }); }); - test("determineBodyResponse returns undefined when the request method is 'HEAD'", function (assert) { + test("determineBodyResponse returns undefined when the request method is 'HEAD' and response is 'null'", function (assert) { assert.expect(1); const response = new Response('null', { status: 200 }); diff --git a/tests/main/tests/unit/utils/parse-response-headers-test.js b/tests/main/tests/unit/utils/parse-response-headers-test.js index d9902b35051..5b1ecadfe50 100644 --- a/tests/main/tests/unit/utils/parse-response-headers-test.js +++ b/tests/main/tests/unit/utils/parse-response-headers-test.js @@ -7,19 +7,19 @@ const LF = '\u000a'; module('unit/adapters/parse-response-headers', function () { test('returns an NULL Object when headersString is undefined', function (assert) { - let headers = parseResponseHeaders(undefined); + const headers = parseResponseHeaders(undefined); assert.deepEqual(headers, Object.create(null), 'NULL Object is returned'); }); test('header parsing', function (assert) { - let headersString = [ + const headersString = [ 'Content-Encoding: gzip', 'content-type: application/json; charset=utf-8', 'date: Fri, 05 Feb 2016 21:47:56 GMT', ].join(CRLF); - let headers = parseResponseHeaders(headersString); + const headers = parseResponseHeaders(headersString); assert.strictEqual(headers['content-encoding'], 'gzip', 'parses basic header pair'); assert.strictEqual(headers['content-type'], 'application/json; charset=utf-8', 'parses header with complex value'); @@ -27,13 +27,13 @@ module('unit/adapters/parse-response-headers', function () { }); test('field-name parsing', function (assert) { - let headersString = [ + const headersString = [ ' name-with-leading-whitespace: some value', 'name-with-whitespace-before-colon : another value', 'Uppercase-Name: yet another value', ].join(CRLF); - let headers = parseResponseHeaders(headersString); + const headers = parseResponseHeaders(headersString); assert.strictEqual( headers['name-with-leading-whitespace'], @@ -49,14 +49,14 @@ module('unit/adapters/parse-response-headers', function () { }); test('field-value parsing', function (assert) { - let headersString = [ + const headersString = [ 'value-with-leading-space: value with leading whitespace', 'value-without-leading-space:value without leading whitespace', 'value-with-colon: value with: a colon', 'value-with-trailing-whitespace: banana ', ].join(CRLF); - let headers = parseResponseHeaders(headersString); + const headers = parseResponseHeaders(headersString); assert.strictEqual( headers['value-with-leading-space'], @@ -82,11 +82,13 @@ module('unit/adapters/parse-response-headers', function () { ('\r\nfoo: bar'); test('ignores headers that do not contain a colon', function (assert) { - let headersString = ['Content-Encoding: gzip', 'I am ignored because I do not contain a colon', 'apple: pie'].join( - CRLF - ); + const headersString = [ + 'Content-Encoding: gzip', + 'I am ignored because I do not contain a colon', + 'apple: pie', + ].join(CRLF); - let headers = parseResponseHeaders(headersString); + const headers = parseResponseHeaders(headersString); assert.deepEqual(headers['content-encoding'], 'gzip', 'parses basic header pair'); assert.deepEqual(headers['apple'], 'pie', 'parses basic header pair'); @@ -94,21 +96,21 @@ module('unit/adapters/parse-response-headers', function () { }); test('tollerate extra new-lines', function (assert) { - let headersString = CRLF + 'foo: bar'; - let headers = parseResponseHeaders(headersString); + const headersString = CRLF + 'foo: bar'; + const headers = parseResponseHeaders(headersString); assert.deepEqual(headers['foo'], 'bar', 'parses basic header pair'); assert.strictEqual(Object.keys(headers).length, 1, 'only has the one valid header'); }); test('works with only line feeds', function (assert) { - let headersString = [ + const headersString = [ 'Content-Encoding: gzip', 'content-type: application/json; charset=utf-8', 'date: Fri, 05 Feb 2016 21:47:56 GMT', ].join(LF); - let headers = parseResponseHeaders(headersString); + const headers = parseResponseHeaders(headersString); assert.strictEqual(headers['Content-Encoding'], 'gzip', 'parses basic header pair'); assert.strictEqual(headers['content-type'], 'application/json; charset=utf-8', 'parses header with complex value'); diff --git a/tests/main/tests/utils/schema.ts b/tests/main/tests/utils/schema.ts new file mode 100644 index 00000000000..20eceeae3b0 --- /dev/null +++ b/tests/main/tests/utils/schema.ts @@ -0,0 +1,181 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + ArrayField, + DerivedField, + FieldSchema, + GenericField, + HashField, + LegacyAttributeField, + LegacyRelationshipSchema, + ObjectField, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + declare _assert: Assert | null; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + this._assert = null; + } + hasTrait(type: string): boolean { + this._assert?.step('TestSchema:hasTrait'); + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + this._assert?.step('TestSchema:resourceHasTrait'); + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(field: GenericField | ObjectField | ArrayField | { type: string }): Transformation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `'${kind}' fields cannot be transformed. Only fields of kind 'field' 'object' or 'array' can specify a transformation. Attempted to find '${field.type ?? ''}' on field '${name}'.`, + !('kind' in field) || ['field', 'object', 'array'].includes(kind) + ); + assert( + `Expected the '${kind}' field '${name}' to specify a transformation via 'field.type', but none was present`, + field.type + ); + assert( + `No transformation registered with name '${field.type}' for '${kind}' field '${name}'`, + this._transforms.has(field.type) + ); + return this._transforms.get(field.type)!; + } + derivation(field: DerivedField | { type: string }): Derivation { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not derived and so cannot be used to lookup a derivation`, + !('kind' in field) || kind === 'derived' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a derivation via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' derivation registered for use by the '${kind}' field '${name}'`, + this._derivations.has(field.type) + ); + return this._derivations.get(field.type)!; + } + hashFn(field: HashField | { type: string }): HashFn { + const kind = 'kind' in field ? field.kind : ''; + const name = 'name' in field ? field.name : ''; + assert( + `The '${kind}' field '${name}' is not a HashField and so cannot be used to lookup a hash function`, + !('kind' in field) || kind === '@hash' + ); + assert( + `Expected the '${kind}' field '${name}' to specify a hash function via 'field.type', but no value was present`, + field.type + ); + assert( + `No '${field.type}' hash function is registered for use by the '${kind}' field '${name}'`, + this._hashFns.has(field.type) + ); + return this._hashFns.get(field.type)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + this._assert?.step('TestSchema:resource'); + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + registerTransformation(transformation: Transformation): void { + this._assert?.step('TestSchema:registerTransformation'); + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._assert?.step('TestSchema:registerDerivation'); + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._assert?.step('TestSchema:registerHashFn'); + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + this._assert?.step('TestSchema:registerResource'); + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + this._assert?.step('TestSchema:registerResources'); + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + this._assert?.step('TestSchema:fields'); + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + this._assert?.step('TestSchema:hasResource'); + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/main/tsconfig.json b/tests/main/tsconfig.json new file mode 100644 index 00000000000..5bf99cce967 --- /dev/null +++ b/tests/main/tsconfig.json @@ -0,0 +1,114 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*", "../../@types/ember-data-qunit-asserts"], + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + // TODO: Reenable this + "noImplicitAny": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types"], + "paths": { + "@ember-data/adapter": ["../../packages/adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../../packages/adapter/unstable-preview-types/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../../packages/serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../../packages/serializer/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/unstable-preview-types/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@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/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"], + "@warp-drive/holodeck/*": ["../../packages/holodeck/unstable-preview-types/*"], + "ember-data": ["../../packages/-ember-data/unstable-preview-types"], + "ember-data/*": ["../../packages/-ember-data/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../packages/build-config" + }, + { + "path": "../../packages/adapter" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/serializer" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/holodeck" + }, + { + "path": "../../packages/-ember-data" + } + ] +} diff --git a/tests/model-encapsulation/.gitignore b/tests/model-encapsulation/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/model-encapsulation/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/model-encapsulation/.template-lintrc.js b/tests/model-encapsulation/.template-lintrc.js deleted file mode 100644 index f35f61c7b3a..00000000000 --- a/tests/model-encapsulation/.template-lintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - extends: 'recommended', -}; diff --git a/tests/model-encapsulation/.watchmanconfig b/tests/model-encapsulation/.watchmanconfig deleted file mode 100644 index e7834e3e4f3..00000000000 --- a/tests/model-encapsulation/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/tests/model-encapsulation/README.md b/tests/model-encapsulation/README.md deleted file mode 100644 index 450c917cc8a..00000000000 --- a/tests/model-encapsulation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# encapsulation-test-app - -This README outlines the details of collaborating on this Ember application. -A short introduction of this app could easily go here. - -## Prerequisites - -You will need the following things properly installed on your computer. - -* [Git](https://git-scm.com/) -* [Node.js](https://nodejs.org/) (with npm) -* [Ember CLI](https://ember-cli.com/) -* [Google Chrome](https://google.com/chrome/) - -## Installation - -* `git clone ` this repository -* `cd encapsulation-test-app` -* `npm install` - -## Running / Development - -* `ember serve` -* Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). - -### Code Generators - -Make use of the many generators for code, try `ember help generate` for more details - -### Running Tests - -* `ember test` -* `ember test --server` - -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - -### Building - -* `ember build` (development) -* `ember build --environment production` (production) - -### Deploying - -Specify what it takes to deploy your app. - -## Further Reading / Useful Links - -* [ember.js](https://emberjs.com/) -* [ember-cli](https://ember-cli.com/) -* Development Browser Extensions - * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) - * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/model-encapsulation/app/app.js b/tests/model-encapsulation/app/app.js deleted file mode 100644 index 1ecb1fb8c84..00000000000 --- a/tests/model-encapsulation/app/app.js +++ /dev/null @@ -1,20 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -window.EmberDataENV = { - ENABLE_OPTIONAL_FEATURES: true, -}; - -const App = Application.extend({ - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix, - Resolver, -}); - -loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/tests/model-encapsulation/app/components/.gitkeep b/tests/model-encapsulation/app/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/app/controllers/.gitkeep b/tests/model-encapsulation/app/controllers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/app/helpers/.gitkeep b/tests/model-encapsulation/app/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/app/index.html b/tests/model-encapsulation/app/index.html deleted file mode 100644 index 181ca7a9f79..00000000000 --- a/tests/model-encapsulation/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EncapsulationTestApp - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/model-encapsulation/app/models/.gitkeep b/tests/model-encapsulation/app/models/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/app/resolver.js b/tests/model-encapsulation/app/resolver.js deleted file mode 100644 index 2fb563d6c04..00000000000 --- a/tests/model-encapsulation/app/resolver.js +++ /dev/null @@ -1,3 +0,0 @@ -import Resolver from 'ember-resolver'; - -export default Resolver; diff --git a/tests/model-encapsulation/app/routes/.gitkeep b/tests/model-encapsulation/app/routes/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/app/templates/components/.gitkeep b/tests/model-encapsulation/app/templates/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/config/environment.js b/tests/model-encapsulation/config/environment.js deleted file mode 100644 index ad6e47878d3..00000000000 --- a/tests/model-encapsulation/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'model-encapsulation-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/model-encapsulation/ember-cli-build.js b/tests/model-encapsulation/ember-cli-build.js deleted file mode 100644 index f748d4fb69c..00000000000 --- a/tests/model-encapsulation/ember-cli-build.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - // allows testing with env config for stripping all deprecations - const compatWith = process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null; - const plugins = [ - ...require('@ember-data/private-build-infra/src/debug-macros')({ - compatWith, - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }), - ]; - - let app = new EmberApp(defaults, { - emberData: { - compatWith, - }, - // Add options here - babel: { - // this ensures that the same build-time code stripping that is done - // for library packages is also done for our tests and dummy app - plugins, - }, - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/tests/model-encapsulation/package.json b/tests/model-encapsulation/package.json deleted file mode 100644 index eab078caabd..00000000000 --- a/tests/model-encapsulation/package.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "model-encapsulation-test-app", - "version": "4.12.8", - "private": true, - "description": "Small description for encapsulation-test-app goes here", - "repository": { - "type": "git", - "url": "https://github.com/emberjs/data.git", - "directory": "tests/model-encapsulation" - }, - "license": "MIT", - "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", - "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/private-build-infra": { - "injected": true - }, - "@ember-data/serializer": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/serializer": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "webpack": "^5.77.0" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "ember": { - "edition": "octane" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/model-encapsulation/public/robots.txt b/tests/model-encapsulation/public/robots.txt deleted file mode 100644 index f5916452e5f..00000000000 --- a/tests/model-encapsulation/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# http://www.robotstxt.org -User-agent: * -Disallow: diff --git a/tests/model-encapsulation/testem.js b/tests/model-encapsulation/testem.js deleted file mode 100644 index 6284ee221b7..00000000000 --- a/tests/model-encapsulation/testem.js +++ /dev/null @@ -1,24 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: ['Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - }, -}; diff --git a/tests/model-encapsulation/tests/helpers/.gitkeep b/tests/model-encapsulation/tests/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/tests/index.html b/tests/model-encapsulation/tests/index.html deleted file mode 100644 index 8a4de471e9e..00000000000 --- a/tests/model-encapsulation/tests/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - EncapsulationTestApp Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
          -
          -
          -
          -
          -
          - - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - diff --git a/tests/model-encapsulation/tests/integration/.gitkeep b/tests/model-encapsulation/tests/integration/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/tests/integration/model-for-test.js b/tests/model-encapsulation/tests/integration/model-for-test.js deleted file mode 100644 index a9a38f9f9ac..00000000000 --- a/tests/model-encapsulation/tests/integration/model-for-test.js +++ /dev/null @@ -1,99 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import Store from '@ember-data/store'; - -module('modelFor without @ember-data/model', function (hooks) { - setupTest(hooks); - - test('We can call modelFor', function (assert) { - this.owner.register( - 'service:store', - class TestStore extends Store { - instantiateRecord() { - return { - id: '1', - type: 'user', - name: 'Chris Thoburn', - }; - } - teardownRecord() { - return; - } - } - ); - const store = this.owner.lookup('service:store'); - store.registerSchemaDefinitionService({ - attributesDefinitionFor(identifier) { - return { - name: { - name: 'name', - }, - }; - }, - relationshipsDefinitionFor(identifier) { - return {}; - }, - doesTypeExist(type) { - return type === 'user'; - }, - }); - - try { - store.modelFor('user'); - assert.ok(true, 'We should not throw an eror when schema is available'); - } catch (e) { - assert.ok(false, `We threw an unexpected error when schema is available: ${e.message}`); - } - - try { - store.modelFor('person'); - assert.ok(false, 'We should throw an eror when no schema is available'); - } catch (e) { - assert.strictEqual( - e.message, - "Assertion Failed: No model was found for 'person' and no schema handles the type", - 'We throw an error when no schema is available' - ); - } - }); - - test('modelFor returns a stable reference', function (assert) { - this.owner.register( - 'service:store', - class TestStore extends Store { - instantiateRecord() { - return { - id: '1', - type: 'user', - name: 'Chris Thoburn', - }; - } - teardownRecord() { - return; - } - } - ); - const store = this.owner.lookup('service:store'); - store.registerSchemaDefinitionService({ - attributesDefinitionFor(identifier) { - return { - name: { - name: 'name', - }, - }; - }, - relationshipsDefinitionFor(identifier) { - return {}; - }, - doesTypeExist(type) { - return type === 'user'; - }, - }); - - const ShimUser1 = store.modelFor('user'); - const ShimUser2 = store.modelFor('user'); - assert.strictEqual(ShimUser1, ShimUser2, 'Repeat modelFor calls return the same shim'); - }); -}); diff --git a/tests/model-encapsulation/tests/integration/smoke-test.js b/tests/model-encapsulation/tests/integration/smoke-test.js deleted file mode 100644 index c82a6baa082..00000000000 --- a/tests/model-encapsulation/tests/integration/smoke-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* global require */ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -function assertPackageNotPresent(packageName, assert) { - const entries = Object.keys(require.entries); - const entriesFromPackage = entries.filter((m) => m.indexOf(packageName) === 0); - const importedDependencies = {}; - const entriesImportingPackage = entries.filter((m) => { - const deps = require.entries[m].deps; - const moduleDeps = deps.filter((d) => d.indexOf(packageName) === 0); - - if (moduleDeps.length) { - importedDependencies[m] = moduleDeps; - } - return moduleDeps.length > 0; - }); - - assert.ok(entries.length > 0, 'We have modules'); - assert.ok( - entriesFromPackage.length === 0, - `We expect no modules from ${packageName} ${ - entriesFromPackage.length > 0 ? `found: [\n\t"${entriesFromPackage.join('",\n\t"')}"\n]` : '' - }` - ); - assert.ok( - entriesImportingPackage.length === 0, - `We expect no modules with dependencies on ${packageName} ${ - entriesImportingPackage.length > 0 ? `found:\n${JSON.stringify(importedDependencies, null, 2)}` : '' - }` - ); -} - -module('Model Encapsulation - Smoke Tests', function (hooks) { - setupTest(hooks); - - test('No @ember-data/model modules are present', function (assert) { - assertPackageNotPresent('@ember-data/model', assert); - }); - - test('No ember-data modules are present', function (assert) { - assertPackageNotPresent('ember-data', assert); - }); -}); diff --git a/tests/model-encapsulation/tests/test-helper.js b/tests/model-encapsulation/tests/test-helper.js deleted file mode 100644 index a16f69329b5..00000000000 --- a/tests/model-encapsulation/tests/test-helper.js +++ /dev/null @@ -1,23 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/tests/model-encapsulation/tests/unit/.gitkeep b/tests/model-encapsulation/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/model-encapsulation/vendor/.gitkeep b/tests/model-encapsulation/vendor/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/performance/app/adapters/application.js b/tests/performance/app/adapters/application.js index 83f27bb1319..7f63157a7fd 100644 --- a/tests/performance/app/adapters/application.js +++ b/tests/performance/app/adapters/application.js @@ -1,7 +1,5 @@ import EmberObject from '@ember/object'; -import { resolve } from 'rsvp'; - export default class ApplicationMockAdapter extends EmberObject { findAll() { return fetch('./fixtures/relationship-materialization-simple.json').then((response) => response.json()); @@ -13,6 +11,6 @@ export default class ApplicationMockAdapter extends EmberObject { return false; } deleteRecord = function () { - return resolve(); + return Promise.resolve(); }; } diff --git a/tests/performance/app/routes/add-children-then-materialize.js b/tests/performance/app/routes/add-children-then-materialize.js index 87267bb99bc..989ddaa7c5f 100644 --- a/tests/performance/app/routes/add-children-then-materialize.js +++ b/tests/performance/app/routes/add-children-then-materialize.js @@ -21,7 +21,7 @@ export default Route.extend({ this.store.peekAll('child').slice(); performance.mark('start-materialize-relationships'); - let seen = new Set(); + const seen = new Set(); peekedParents.forEach((parent) => iterateParent(parent, seen)); performance.mark('end-materialize-relationships'); diff --git a/tests/performance/app/routes/destroy.js b/tests/performance/app/routes/destroy.js index 159bb96725b..3e9356aa91b 100644 --- a/tests/performance/app/routes/destroy.js +++ b/tests/performance/app/routes/destroy.js @@ -1,8 +1,6 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; -import { all } from 'rsvp'; - export default Route.extend({ store: service(), @@ -15,10 +13,10 @@ export default Route.extend({ performance.mark('start-destroy-records'); const children = await parent.children; - const childrenPromise = all(children.slice().map((child) => child.destroyRecord())); + const childrenPromise = Promise.all(children.slice().map((child) => child.destroyRecord())); const parentPromise = parent.destroyRecord(); - await all([childrenPromise, parentPromise]); + await Promise.all([childrenPromise, parentPromise]); performance.mark('end-destroy-records'); }, diff --git a/tests/performance/app/routes/relationship-materialization-complex.js b/tests/performance/app/routes/relationship-materialization-complex.js index c9db131cdab..7892858bc43 100644 --- a/tests/performance/app/routes/relationship-materialization-complex.js +++ b/tests/performance/app/routes/relationship-materialization-complex.js @@ -16,7 +16,7 @@ export default Route.extend({ peekedChildren.slice(); peekedParents.slice(); performance.mark('start-relationship-materialization'); - let seen = new Set(); + const seen = new Set(); peekedParents.forEach((parent) => iterateParent(parent, seen)); performance.mark('end-relationship-materialization'); }, diff --git a/tests/performance/app/routes/unload-all.js b/tests/performance/app/routes/unload-all.js index 02a4ece9679..da4e567871d 100644 --- a/tests/performance/app/routes/unload-all.js +++ b/tests/performance/app/routes/unload-all.js @@ -1,5 +1,4 @@ import Route from '@ember/routing/route'; -import { run } from '@ember/runloop'; import { inject as service } from '@ember/service'; export default Route.extend({ @@ -15,9 +14,7 @@ export default Route.extend({ this.store.peekAll('parent').slice(); performance.mark('start-unload-all'); - run(() => { - this.store.unloadAll(); - }); + this.store.unloadAll(); performance.mark('end-unload-all'); }, }); diff --git a/tests/performance/app/routes/unload.js b/tests/performance/app/routes/unload.js index 871250753ce..9e3326942a4 100644 --- a/tests/performance/app/routes/unload.js +++ b/tests/performance/app/routes/unload.js @@ -1,5 +1,4 @@ import Route from '@ember/routing/route'; -import { run } from '@ember/runloop'; import { inject as service } from '@ember/service'; export default Route.extend({ @@ -14,9 +13,7 @@ export default Route.extend({ const children = await parent.children; // runloop to ensure destroy does not escape bounds of the test - run(() => { - children.slice().forEach((child) => child.unloadRecord()); - }); + children.slice().forEach((child) => child.unloadRecord()); performance.mark('end-unload-records'); }, }); diff --git a/tests/performance/config/environment.js b/tests/performance/config/environment.js index 317915898dd..d5b7e8d3b9f 100644 --- a/tests/performance/config/environment.js +++ b/tests/performance/config/environment.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = function (environment) { - let ENV = { + const ENV = { modulePrefix: 'performance-test-app', environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build diff --git a/tests/performance/ember-cli-build.js b/tests/performance/ember-cli-build.js index 041841ce912..7896757c1ef 100644 --- a/tests/performance/ember-cli-build.js +++ b/tests/performance/ember-cli-build.js @@ -1,11 +1,9 @@ -/* eslint node/no-unpublished-require: 'off' */ - 'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); module.exports = function (defaults) { - let app = new EmberApp(defaults, { + const app = new EmberApp(defaults, { fingerprint: { enabled: false, }, @@ -27,5 +25,58 @@ module.exports = function (defaults) { // please specify an object with the list of modules as keys // along with the exports of each module as its value. - return app.toTree(); + const { Webpack } = require('@embroider/webpack'); + const TerserPlugin = require('terser-webpack-plugin'); + + return require('@embroider/compat').compatBuild(app, Webpack, { + // + // staticAddonTestSupportTrees: true, + // staticAddonTrees: true, + // staticHelpers: true, + // staticModifiers: true, + // staticComponents: true, + // splitAtRoutes: ['route.name'], // can also be a RegExp + packagerOptions: { + webpackConfig: { + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + compress: { + ecma: 2022, + passes: 6, // slow, but worth it + negate_iife: false, + sequences: 30, + defaults: true, + arguments: false, + keep_fargs: false, + toplevel: false, + unsafe: true, + unsafe_comps: true, + unsafe_math: true, + unsafe_symbols: true, + unsafe_proto: true, + unsafe_undefined: true, + inline: 5, + reduce_funcs: false, + }, + mangle: { + keep_classnames: true, + keep_fnames: true, + module: true, + }, + format: { beautify: true }, + toplevel: false, + sourceMap: false, + ecma: 2022, + }, + }), + ], + }, + }, + }, + // + extraPublicTrees: [], + }); }; diff --git a/tests/performance/eslint.config.mjs b/tests/performance/eslint.config.mjs new file mode 100644 index 00000000000..58117e3da50 --- /dev/null +++ b/tests/performance/eslint.config.mjs @@ -0,0 +1,24 @@ +// @ts-check +import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; +import * as node from '@warp-drive/internal-config/eslint/node.js'; +import * as js from '@warp-drive/internal-config/eslint/browser.js'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + // all ================ + globalIgnores(), + + // browser (js/ts) ================ + js.browser({ + srcDirs: ['app'], + allowedImports: ['@ember/application', '@ember/object', '@ember/routing/route', '@ember/service'], + }), + + // node (module) ================ + node.esm(), + + // node (script) ================ + node.cjs({ + files: ['server/**/*.js', 'fixtures/**/*.js'], + }), +]; diff --git a/tests/performance/fixtures/create-parent-records.js b/tests/performance/fixtures/create-parent-records.js index 52f912430f7..bc83f6de65c 100644 --- a/tests/performance/fixtures/create-parent-records.js +++ b/tests/performance/fixtures/create-parent-records.js @@ -12,7 +12,7 @@ module.exports = function createParentRecords(nrParents = 1, nrChildren, nrFrien }; for (let i = 0; i < nrParents; i++) { - let payload = createParentPayload(`${parentFixtureId++}`, nrChildren, nrFriends); + const payload = createParentPayload(`${parentFixtureId++}`, nrChildren, nrFriends); if (nrParents === 1) { return payload; @@ -55,7 +55,7 @@ function createParentPayload(parentId = '1', nrChildren = 0, nrFriends = 0) { const childIdentifier = extractIdentifiers(child); if (nrFriends > 0) { - let bestFriend = ALL_FRIENDS[friendIndex]; + const bestFriend = ALL_FRIENDS[friendIndex]; child.relationships.bestFriend = { data: extractIdentifiers(bestFriend), }; @@ -67,7 +67,7 @@ function createParentPayload(parentId = '1', nrChildren = 0, nrFriends = 0) { data: otherFriends, }; for (let i = 0; i < nrFriends; i++) { - let friend = ALL_FRIENDS[friendIndex + i]; + const friend = ALL_FRIENDS[friendIndex + i]; friend.relationships.friends = { data: [childIdentifier], }; @@ -75,7 +75,7 @@ function createParentPayload(parentId = '1', nrChildren = 0, nrFriends = 0) { } } if (nrFriends > 1) { - let secondBestFriend = ALL_FRIENDS[friendIndex + 1]; + const secondBestFriend = ALL_FRIENDS[friendIndex + 1]; child.relationships.secondBestFriend = { data: extractIdentifiers(secondBestFriend), }; diff --git a/tests/performance/fixtures/index.js b/tests/performance/fixtures/index.js index 2365b7ee723..ee190736ec2 100644 --- a/tests/performance/fixtures/index.js +++ b/tests/performance/fixtures/index.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const fs = require('fs'); const zlib = require('zlib'); diff --git a/tests/performance/package.json b/tests/performance/package.json index bb92a2bc837..4a551b41c0f 100644 --- a/tests/performance/package.json +++ b/tests/performance/package.json @@ -16,12 +16,15 @@ }, "scripts": { "build": "ember build", - "start": "ember serve" + "start": "ember serve", + "lint": "eslint . --quiet --cache --cache-strategy=content", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, "dependencies": { - "ember-auto-import": "^2.6.1", - "ember-data": "workspace:4.12.8", - "@ember/string": "^4.0.0" + "ember-auto-import": "^2.8.1", + "ember-data": "workspace:*", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "webpack": "^5.92.0" }, "dependenciesMeta": { "ember-data": { @@ -29,35 +32,38 @@ } }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.9.3", + "@babel/core": "^7.24.5", + "@babel/runtime": "^7.24.5", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "4.0.4", + "@ember/test-waiters": "^3.1.0", + "@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", - "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-export-application-global": "^2.0.1", + "@warp-drive/internal-config": "workspace:*", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", + "ember-resolver": "^11.0.1", + "ember-source": "~5.12.0", "loader.js": "^4.7.0", - "webpack": "^5.77.0", + "terser-webpack-plugin": "^5.3.10", + "webpack": "^5.92.0", "zlib": "1.0.5" }, "ember": { "edition": "octane" }, "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" + "node": ">= 18.20.4" }, "volta": { "extends": "../../package.json" }, - "packageManager": "pnpm@9.7.1" + "packageManager": "pnpm@8.15.9" } diff --git a/tests/request/README.md b/tests/request/README.md deleted file mode 100644 index 21e3f562a0e..00000000000 --- a/tests/request/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# request-tests - -Provides testing for the RequestManager diff --git a/tests/request/app/app.ts b/tests/request/app/app.ts deleted file mode 100644 index 1f39476ae86..00000000000 --- a/tests/request/app/app.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -class App extends Application { - modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; - Resolver = Resolver; -} - -loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/tests/request/app/index.html b/tests/request/app/index.html deleted file mode 100644 index e46e09bc3cb..00000000000 --- a/tests/request/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EmberData RequestManager Test App - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/request/app/templates/.gitkeep b/tests/request/app/templates/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/request/app/templates/application.hbs b/tests/request/app/templates/application.hbs deleted file mode 100644 index 578920ea827..00000000000 --- a/tests/request/app/templates/application.hbs +++ /dev/null @@ -1,7 +0,0 @@ -
          -

          EmberData Graph Tests

          - - {{outlet}} - - Tests -
          diff --git a/tests/request/config/environment.js b/tests/request/config/environment.js deleted file mode 100644 index 0caf91a5989..00000000000 --- a/tests/request/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'request-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/request/config/optional-features.json b/tests/request/config/optional-features.json deleted file mode 100644 index b26286e2ecd..00000000000 --- a/tests/request/config/optional-features.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "application-template-wrapper": false, - "default-async-observers": true, - "jquery-integration": false, - "template-only-glimmer-components": true -} diff --git a/tests/request/config/targets.js b/tests/request/config/targets.js deleted file mode 100644 index b6756da2517..00000000000 --- a/tests/request/config/targets.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -let browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; -const isProd = process.env.EMBER_ENV === 'production'; - -if (isProd) { - browsers = ['last 2 Chrome versions', 'last 2 Firefox versions', 'Safari 12', 'last 2 Edge versions']; -} - -module.exports = { - browsers, - node: 'current', -}; diff --git a/tests/request/ember-cli-build.js b/tests/request/ember-cli-build.js deleted file mode 100644 index 2fa1e887d48..00000000000 --- a/tests/request/ember-cli-build.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - const compatWith = process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null; - let app = new EmberApp(defaults, { - emberData: { - compatWith, - }, - babel: { - // this ensures that the same build-time code stripping that is done - // for library packages is also done for our tests and dummy app - plugins: [ - ...require('@ember-data/private-build-infra/src/debug-macros')({ - compatWith, - debug: {}, - features: {}, - deprecations: {}, - env: require('@ember-data/private-build-infra/src/utilities/get-env')(), - }), - ], - }, - 'ember-cli-babel': { - throwUnlessParallelizable: true, - enableTypeScriptTransform: true, - }, - 'ember-cli-terser': { - exclude: ['assets/dummy.js', 'assets/tests.js', 'assets/test-support.js'], - }, - }); - - /* - This build file specifies the options for the dummy test app of this - addon, located in `/tests/dummy` - This build file does *not* influence how the addon or the app using it - behave. You most likely want to be modifying `./index.js` or app's build file - */ - - return app.toTree(); -}; diff --git a/tests/request/package.json b/tests/request/package.json deleted file mode 100644 index 3753b89e305..00000000000 --- a/tests/request/package.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "request-test-app", - "version": "4.12.8", - "description": "Provides tests for the RequestManager", - "keywords": [], - "repository": { - "type": "git", - "url": "git+ssh://git@github.com:emberjs/data.git", - "directory": "tests/request" - }, - "license": "MIT", - "author": "", - "directories": { - "test": "tests" - }, - "scripts": { - "build": "ember build", - "start": "ember test --test-port=0 --serve --no-launch", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/request": { - "injected": true - }, - "@ember-data/private-build-infra": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/private-build-infra": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/edition-utils": "^1.2.0", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-blueprint-test-helpers": "^0.19.2", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-sri": "^2.1.1", - "ember-cli-terser": "~4.0.2", - "ember-cli-test-loader": "^3.0.0", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "ember-source-channel-url": "^3.0.0", - "ember-try": "^2.0.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-console-grouper": "^0.3.0", - "qunit-dom": "^2.0.0", - "silent-error": "^1.1.1", - "webpack": "^5.77.0" - }, - "ember": { - "edition": "octane" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/request/testem.js b/tests/request/testem.js deleted file mode 100644 index a85540f243d..00000000000 --- a/tests/request/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable node/no-unpublished-require */ -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - Firefox: { - ci: ['--headless', '--width=1440', '--height=900'], - }, - }, -}; diff --git a/tests/request/tests/.gitkeep b/tests/request/tests/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/request/tests/index.html b/tests/request/tests/index.html deleted file mode 100644 index 46753b4f41c..00000000000 --- a/tests/request/tests/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - RequestManager Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
          -
          -
          -
          -
          -
          - - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/tests/request/tests/integration/request-manager-test.ts b/tests/request/tests/integration/request-manager-test.ts deleted file mode 100644 index 4e3b7d7c25f..00000000000 --- a/tests/request/tests/integration/request-manager-test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { module, test } from 'qunit'; - -module('RequestManager', function () { - test('Test Suit Configured', function (assert) { - assert.ok('We are configured'); - }); -}); diff --git a/tests/request/tests/integration/response-currying-test.ts b/tests/request/tests/integration/response-currying-test.ts deleted file mode 100644 index a943640dc97..00000000000 --- a/tests/request/tests/integration/response-currying-test.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { module, test } from 'qunit'; - -import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Handler, NextFn } from '@ember-data/request/-private/types'; - -module('RequestManager | Response Currying', function () { - test('We curry response when setResponse is not called', async function (assert) { - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn) { - const response = await next(context.request); - return response.content; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; - // @ts-expect-error - serialized.headers = (serialized.headers as [string, string][]).filter((v) => { - // don't test headers that change every time - return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); - }); - // @ts-expect-error port is unstable in CI - delete serialized.url; - - assert.deepEqual( - serialized, - { - ok: true, - redirected: false, - headers: [ - ['accept-ranges', 'bytes'], - ['cache-control', 'public, max-age=0'], - ['content-type', 'application/json; charset=UTF-8'], - // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], - // ['etag', 'W/"39-1849db13af9"'], - // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], - ['vary', 'Accept-Encoding'], - ['x-powered-by', 'Express'], - ], - status: 200, - statusText: 'OK', - type: 'basic', - }, - 'The response is processed correctly' - ); - }); - - test('We do not curry response when we call next multiple times', async function (assert) { - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn): Promise { - await next(context.request); - await next(context.request); - return (await next(context.request)).content; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - - assert.strictEqual(doc.response, null, 'The response is processed correctly'); - }); - - test('We curry when we return directly', async function (assert) { - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn): Promise { - return next(context.request) as unknown as Promise; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; - // @ts-expect-error - serialized.headers = (serialized.headers as [string, string][]).filter((v) => { - // don't test headers that change every time - return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); - }); - // @ts-expect-error port is unstable in CI - delete serialized.url; - - assert.deepEqual( - serialized, - { - ok: true, - redirected: false, - headers: [ - ['accept-ranges', 'bytes'], - ['cache-control', 'public, max-age=0'], - ['content-type', 'application/json; charset=UTF-8'], - // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], - // ['etag', 'W/"39-1849db13af9"'], - // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], - ['vary', 'Accept-Encoding'], - ['x-powered-by', 'Express'], - ], - status: 200, - statusText: 'OK', - type: 'basic', - }, - 'The response is processed correctly' - ); - }); - - test('We can intercept Response', async function (assert) { - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn): Promise { - const doc = await next(context.request); - - const response = Object.assign({}, doc.response, { ok: false }); - context.setResponse(response); - - return doc.content; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; - // @ts-expect-error - serialized.headers = (serialized.headers as [string, string][]).filter((v) => { - // don't test headers that change every time - return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); - }); - // @ts-expect-error port is unstable in CI - delete serialized.url; - - assert.deepEqual( - serialized, - { - ok: false, - redirected: false, - headers: [ - ['accept-ranges', 'bytes'], - ['cache-control', 'public, max-age=0'], - ['content-type', 'application/json; charset=UTF-8'], - // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], - // ['etag', 'W/"39-1849db13af9"'], - // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], - ['vary', 'Accept-Encoding'], - ['x-powered-by', 'Express'], - ], - status: 200, - statusText: 'OK', - type: 'basic', - }, - 'The response is processed correctly' - ); - }); - - test("We can can't mutate Response", async function (assert) { - assert.expect(3); - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn): Promise { - const doc = await next(context.request); - - try { - // @ts-expect-error - doc.response!.ok = false; - assert.ok(false, 'we should be immutable'); - } catch (e) { - assert.ok(true, 'we are immutable'); - } - - try { - doc.response!.headers.append('foo', 'bar'); - assert.ok(false, 'we should be immutable'); - } catch (e) { - assert.ok(true, 'we are immutable'); - } - - return doc.content; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; - // @ts-expect-error - serialized.headers = (serialized.headers as [string, string][]).filter((v) => { - // don't test headers that change every time - return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); - }); - // @ts-expect-error port is unstable in CI - delete serialized.url; - - assert.deepEqual( - serialized, - { - ok: true, - redirected: false, - headers: [ - ['accept-ranges', 'bytes'], - ['cache-control', 'public, max-age=0'], - ['content-type', 'application/json; charset=UTF-8'], - // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], - // ['etag', 'W/"39-1849db13af9"'], - // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], - ['vary', 'Accept-Encoding'], - ['x-powered-by', 'Express'], - ], - status: 200, - statusText: 'OK', - type: 'basic', - }, - 'The response is processed correctly' - ); - }); - - test('We can set response to null', async function (assert) { - const manager = new RequestManager(); - const handler1: Handler = { - async request(context: Context, next: NextFn): Promise { - const doc = await next(context.request); - - context.setResponse(null); - - return doc.content; - }, - }; - const handler2: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler1, handler2]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - - assert.strictEqual(doc.response, null, 'The response is processed correctly'); - }); -}); diff --git a/tests/request/tests/integration/response-test.ts b/tests/request/tests/integration/response-test.ts deleted file mode 100644 index 08db5a048ba..00000000000 --- a/tests/request/tests/integration/response-test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { module, test } from 'qunit'; - -import RequestManager from '@ember-data/request'; -import type { Context } from '@ember-data/request/-private/context'; -import type { Handler, NextFn } from '@ember-data/request/-private/types'; - -module('RequestManager | Response', function () { - test('Handlers may set response via Response', async function (assert) { - const manager = new RequestManager(); - const handler: Handler = { - async request(context: Context, next: NextFn) { - const response = await fetch(context.request.url!, context.request); - context.setResponse(response); - return response.json(); - }, - }; - manager.use([handler]); - - const doc = await manager.request({ url: '../assets/demo-fetch.json' }); - const serialized = JSON.parse(JSON.stringify(doc.response)) as unknown; - // @ts-expect-error - serialized.headers = (serialized.headers as [string, string][]).filter((v) => { - // don't test headers that change every time - return !['content-length', 'date', 'etag', 'last-modified'].includes(v[0]); - }); - // @ts-expect-error port is unstable in CI - delete serialized.url; - - assert.deepEqual( - serialized, - { - ok: true, - redirected: false, - headers: [ - ['accept-ranges', 'bytes'], - ['cache-control', 'public, max-age=0'], - ['content-type', 'application/json; charset=UTF-8'], - // ['date', 'Wed, 23 Nov 2022 05:17:11 GMT'], - // ['etag', 'W/"39-1849db13af9"'], - // ['last-modified', 'Tue, 22 Nov 2022 04:55:48 GMT'], - ['vary', 'Accept-Encoding'], - ['x-powered-by', 'Express'], - ], - status: 200, - statusText: 'OK', - type: 'basic', - }, - 'The response is processed correctly' - ); - }); -}); diff --git a/tests/request/tests/integration/service-test.ts b/tests/request/tests/integration/service-test.ts deleted file mode 100644 index 7e7fa3c1447..00000000000 --- a/tests/request/tests/integration/service-test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -import { getOwner } from '@ember/application'; -import Service, { inject as service } from '@ember/service'; -import { TestContext } from '@ember/test-helpers'; - -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -import RequestManager from '@ember-data/request'; - -module('RequestManager | Ember Service Setup', function (hooks: NestedHooks) { - setupTest(hooks); - - test('We can register RequestManager as a service', function (this: TestContext, assert: Assert) { - this.owner.register('service:request', RequestManager); - const manager = this.owner.lookup('service:request'); - assert.ok(manager instanceof RequestManager, 'We instantiated'); - }); - - test('We can use injections when registering the RequestManager as a service', function (this: TestContext, assert: Assert) { - class CustomManager extends RequestManager { - @service cache; - } - this.owner.register('service:request', CustomManager); - class Cache extends Service {} - this.owner.register('service:cache', Cache); - const manager = this.owner.lookup('service:request') as CustomManager; - assert.ok(manager instanceof RequestManager, 'We instantiated'); - assert.ok(manager instanceof CustomManager, 'We instantiated'); - assert.ok(manager.cache instanceof Cache, 'We can utilize injections'); - assert.strictEqual(getOwner(manager), this.owner, 'The manager correctly sets owner'); - }); -}); diff --git a/tests/request/tests/integration/setup-test.ts b/tests/request/tests/integration/setup-test.ts deleted file mode 100644 index 2308cdca439..00000000000 --- a/tests/request/tests/integration/setup-test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { module, test } from 'qunit'; - -import RequestManager from '@ember-data/request'; -import type { Context as HandlerRequestContext } from '@ember-data/request/-private/context'; -import type { NextFn } from '@ember-data/request/-private/types'; - -module('RequestManager | Basic Setup', function () { - test('We can call new RequestManager() with no args', function (assert) { - const manager = new RequestManager(); - assert.ok(manager instanceof RequestManager, 'We instantiated'); - }); - - test('We can call RequestManager.create() with no args', function (assert) { - const manager = RequestManager.create(); - assert.ok(manager instanceof RequestManager, 'We instantiated'); - }); - - test('We can register a handler with `.use()`', async function (assert) { - const manager = new RequestManager(); - let calls = 0; - manager.use([ - { - request(req: HandlerRequestContext, next: NextFn) { - calls++; - return Promise.resolve('success!' as T); - }, - }, - ]); - const req = { - url: '/foos', - }; - const result = await manager.request(req); - assert.strictEqual(calls, 1, 'we called our handler'); - assert.strictEqual(JSON.stringify(result.request), JSON.stringify(req)); - assert.strictEqual(result.content, 'success!', 'we returned the expected result'); - }); - - test('We can register multiple handlers with `.use()`', async function (assert) { - const manager = new RequestManager(); - let calls = 0; - let callsB = 0; - manager.use([ - { - async request(req: HandlerRequestContext, next: NextFn) { - calls++; - const outcome = await next(req.request); - return outcome.content; - }, - }, - { - request(req: HandlerRequestContext, next: NextFn) { - callsB++; - return Promise.resolve('success!' as T); - }, - }, - ]); - const req = { - url: '/foos', - }; - const result = await manager.request(req); - assert.strictEqual(calls, 1, 'we called our handler'); - assert.strictEqual(callsB, 1, 'we called our next handler'); - assert.strictEqual(JSON.stringify(result.request), JSON.stringify(req)); - assert.strictEqual(result.content, 'success!', 'we returned the expected result'); - }); - - test('We can register the same handler more than once with `.use()`', async function (assert) { - const manager = new RequestManager(); - let calls = 0; - - const handler = { - async request(req: HandlerRequestContext, next: NextFn) { - calls++; - if (calls === 2) { - return Promise.resolve('success!' as T); - } - const outcome = await next(req.request); - return outcome.content; - }, - }; - - manager.use([handler, handler]); - const req = { - url: '/foos', - }; - const result = await manager.request(req); - assert.strictEqual(calls, 2, 'we called our handler'); - assert.strictEqual(JSON.stringify(result.request), JSON.stringify(req)); - assert.strictEqual(result.content, 'success!', 'we returned the expected result'); - }); -}); diff --git a/tests/request/tests/test-helper.js b/tests/request/tests/test-helper.js deleted file mode 100644 index 147cf2a3d4d..00000000000 --- a/tests/request/tests/test-helper.js +++ /dev/null @@ -1,54 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; -import RSVP from 'rsvp'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; -import customQUnitAdapter from '@ember-data/unpublished-test-infra/test-support/testem/custom-qunit-adapter'; - -import Application from '../app'; -import config from '../config/environment'; - -if (window.Promise === undefined) { - window.Promise = RSVP.Promise; -} - -// Handle testing feature flags -if (QUnit.urlParams.enableoptionalfeatures) { - window.EmberDataENV = { ENABLE_OPTIONAL_FEATURES: true }; -} - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -if (window.Testem) { - window.Testem.useCustomAdapter(customQUnitAdapter); -} - -QUnit.begin(function () { - RSVP.configure('onerror', (reason) => { - // only print error messages if they're exceptions; - // otherwise, let a future turn of the event loop - // handle the error. - // TODO kill this off - if (reason && reason instanceof Error) { - throw reason; - } - }); -}); - -QUnit.config.testTimeout = 2000; -QUnit.config.urlConfig.push({ - id: 'enableoptionalfeatures', - label: 'Enable Opt Features', -}); -start({ setupTestIsolationValidation: true }); diff --git a/tests/serializer-encapsulation/.ember-cli b/tests/serializer-encapsulation/.ember-cli deleted file mode 100644 index ee64cfed2a8..00000000000 --- a/tests/serializer-encapsulation/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/tests/serializer-encapsulation/.gitignore b/tests/serializer-encapsulation/.gitignore deleted file mode 100644 index c40a1b2aba3..00000000000 --- a/tests/serializer-encapsulation/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/tests/serializer-encapsulation/.template-lintrc.js b/tests/serializer-encapsulation/.template-lintrc.js deleted file mode 100644 index f35f61c7b3a..00000000000 --- a/tests/serializer-encapsulation/.template-lintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - extends: 'recommended', -}; diff --git a/tests/serializer-encapsulation/.watchmanconfig b/tests/serializer-encapsulation/.watchmanconfig deleted file mode 100644 index e7834e3e4f3..00000000000 --- a/tests/serializer-encapsulation/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/tests/serializer-encapsulation/README.md b/tests/serializer-encapsulation/README.md deleted file mode 100644 index 450c917cc8a..00000000000 --- a/tests/serializer-encapsulation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# encapsulation-test-app - -This README outlines the details of collaborating on this Ember application. -A short introduction of this app could easily go here. - -## Prerequisites - -You will need the following things properly installed on your computer. - -* [Git](https://git-scm.com/) -* [Node.js](https://nodejs.org/) (with npm) -* [Ember CLI](https://ember-cli.com/) -* [Google Chrome](https://google.com/chrome/) - -## Installation - -* `git clone ` this repository -* `cd encapsulation-test-app` -* `npm install` - -## Running / Development - -* `ember serve` -* Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). - -### Code Generators - -Make use of the many generators for code, try `ember help generate` for more details - -### Running Tests - -* `ember test` -* `ember test --server` - -### Linting - -* `npm run lint:hbs` -* `npm run lint:js` -* `npm run lint:js -- --fix` - -### Building - -* `ember build` (development) -* `ember build --environment production` (production) - -### Deploying - -Specify what it takes to deploy your app. - -## Further Reading / Useful Links - -* [ember.js](https://emberjs.com/) -* [ember-cli](https://ember-cli.com/) -* Development Browser Extensions - * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) - * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/serializer-encapsulation/app/app.js b/tests/serializer-encapsulation/app/app.js deleted file mode 100644 index b73a4fb0677..00000000000 --- a/tests/serializer-encapsulation/app/app.js +++ /dev/null @@ -1,16 +0,0 @@ -import Application from '@ember/application'; - -import loadInitializers from 'ember-load-initializers'; - -import config from './config/environment'; -import Resolver from './resolver'; - -const App = Application.extend({ - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix, - Resolver, -}); - -loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/tests/serializer-encapsulation/app/components/.gitkeep b/tests/serializer-encapsulation/app/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/controllers/.gitkeep b/tests/serializer-encapsulation/app/controllers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/helpers/.gitkeep b/tests/serializer-encapsulation/app/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/index.html b/tests/serializer-encapsulation/app/index.html deleted file mode 100644 index 4760bdac589..00000000000 --- a/tests/serializer-encapsulation/app/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - EncapsulationTestApp - - - - {{content-for "head"}} - - - - - {{content-for "head-footer"}} - - - {{content-for "body"}} - - - - - {{content-for "body-footer"}} - - diff --git a/tests/serializer-encapsulation/app/models/.gitkeep b/tests/serializer-encapsulation/app/models/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/resolver.js b/tests/serializer-encapsulation/app/resolver.js deleted file mode 100644 index 2fb563d6c04..00000000000 --- a/tests/serializer-encapsulation/app/resolver.js +++ /dev/null @@ -1,3 +0,0 @@ -import Resolver from 'ember-resolver'; - -export default Resolver; diff --git a/tests/serializer-encapsulation/app/router.js b/tests/serializer-encapsulation/app/router.js deleted file mode 100644 index 7525f056ab3..00000000000 --- a/tests/serializer-encapsulation/app/router.js +++ /dev/null @@ -1,12 +0,0 @@ -import EmberRouter from '@ember/routing/router'; - -import config from './config/environment'; - -const Router = EmberRouter.extend({ - location: config.locationType, - rootURL: config.rootURL, -}); - -Router.map(function () {}); - -export default Router; diff --git a/tests/serializer-encapsulation/app/routes/.gitkeep b/tests/serializer-encapsulation/app/routes/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/services/store.js b/tests/serializer-encapsulation/app/services/store.js deleted file mode 100644 index c8063c361fd..00000000000 --- a/tests/serializer-encapsulation/app/services/store.js +++ /dev/null @@ -1,16 +0,0 @@ -import Cache from '@ember-data/json-api'; -import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; -import RequestManager from '@ember-data/request'; -import Store, { CacheHandler } from '@ember-data/store'; - -export default class DefaultStore extends Store { - constructor() { - super(...arguments); - this.requestManager = new RequestManager(); - this.requestManager.use([LegacyNetworkHandler]); - this.requestManager.useCache(CacheHandler); - } - createCache(storeWrapper) { - return new Cache(storeWrapper); - } -} diff --git a/tests/serializer-encapsulation/app/styles/app.css b/tests/serializer-encapsulation/app/styles/app.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/app/templates/application.hbs b/tests/serializer-encapsulation/app/templates/application.hbs deleted file mode 100644 index ebe6a496046..00000000000 --- a/tests/serializer-encapsulation/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
          - {{outlet}} -
          \ No newline at end of file diff --git a/tests/serializer-encapsulation/app/templates/components/.gitkeep b/tests/serializer-encapsulation/app/templates/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/config/environment.js b/tests/serializer-encapsulation/config/environment.js deleted file mode 100644 index aa1f5bb9a8e..00000000000 --- a/tests/serializer-encapsulation/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -module.exports = function (environment) { - let ENV = { - modulePrefix: 'serializer-encapsulation-test-app', - environment, - rootURL: '/', - locationType: 'auto', - EmberENV: { - FEATURES: { - // Here you can enable experimental features on an ember canary build - // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true - }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, - }, - - APP: { - // Here you can pass flags/options to your application instance - // when it is created - }, - }; - - if (environment === 'development') { - // ENV.APP.LOG_RESOLVER = true; - // ENV.APP.LOG_ACTIVE_GENERATION = true; - // ENV.APP.LOG_TRANSITIONS = true; - // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; - // ENV.APP.LOG_VIEW_LOOKUPS = true; - } - - if (environment === 'test') { - // Testem prefers this... - ENV.locationType = 'none'; - - // keep test console output quieter - ENV.APP.LOG_ACTIVE_GENERATION = false; - ENV.APP.LOG_VIEW_LOOKUPS = false; - - ENV.APP.rootElement = '#ember-testing'; - ENV.APP.autoboot = false; - } - - if (environment === 'production') { - // here you can enable a production-specific feature - } - - return ENV; -}; diff --git a/tests/serializer-encapsulation/config/optional-features.json b/tests/serializer-encapsulation/config/optional-features.json deleted file mode 100644 index b26286e2ecd..00000000000 --- a/tests/serializer-encapsulation/config/optional-features.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "application-template-wrapper": false, - "default-async-observers": true, - "jquery-integration": false, - "template-only-glimmer-components": true -} diff --git a/tests/serializer-encapsulation/config/targets.js b/tests/serializer-encapsulation/config/targets.js deleted file mode 100644 index b6756da2517..00000000000 --- a/tests/serializer-encapsulation/config/targets.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -let browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; -const isProd = process.env.EMBER_ENV === 'production'; - -if (isProd) { - browsers = ['last 2 Chrome versions', 'last 2 Firefox versions', 'Safari 12', 'last 2 Edge versions']; -} - -module.exports = { - browsers, - node: 'current', -}; diff --git a/tests/serializer-encapsulation/ember-cli-build.js b/tests/serializer-encapsulation/ember-cli-build.js deleted file mode 100644 index 8f86cbd8ec6..00000000000 --- a/tests/serializer-encapsulation/ember-cli-build.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint node/no-unpublished-require: 'off' */ - -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - let app = new EmberApp(defaults, { - // Add options here - }); - - // Use `app.import` to add additional libraries to the generated - // output files. - // - // If you need to use different assets in different - // environments, specify an object as the first parameter. That - // object's keys should be the environment name and the values - // should be the asset to use in that environment. - // - // If the library that you are including contains AMD or ES6 - // modules that you would like to import into your application - // please specify an object with the list of modules as keys - // along with the exports of each module as its value. - - return app.toTree(); -}; diff --git a/tests/serializer-encapsulation/package.json b/tests/serializer-encapsulation/package.json deleted file mode 100644 index 720aed03d82..00000000000 --- a/tests/serializer-encapsulation/package.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "name": "serializer-encapsulation-test-app", - "version": "4.12.8", - "private": true, - "description": "Small description for encapsulation-test-app goes here", - "repository": { - "type": "git", - "url": "https://github.com/emberjs/data.git", - "directory": "tests/serializer-encapsulation" - }, - "license": "MIT", - "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "lint:hbs": "ember-template-lint .", - "lint:js": "eslint --config ../../.eslintrc.js --ignore-path ../../.eslintignore .", - "start": "ember serve", - "test": "ember test --test-port=0" - }, - "dependenciesMeta": { - "@ember-data/adapter": { - "injected": true - }, - "@ember-data/model": { - "injected": true - }, - "@ember-data/json-api": { - "injected": true - }, - "@ember-data/store": { - "injected": true - }, - "@ember-data/request": { - "injected": true - }, - "@ember-data/legacy-compat": { - "injected": true - }, - "@ember-data/graph": { - "injected": true - }, - "@ember-data/tracking": { - "injected": true - }, - "@ember-data/unpublished-test-infra": { - "injected": true - } - }, - "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/runtime": "^7.21.0", - "@ember-data/adapter": "workspace:4.12.8", - "@ember-data/model": "workspace:4.12.8", - "@ember-data/json-api": "workspace:4.12.8", - "@ember-data/graph": "workspace:4.12.8", - "@ember-data/store": "workspace:4.12.8", - "@ember-data/legacy-compat": "workspace:4.12.8", - "@ember-data/request": "workspace:4.12.8", - "@ember-data/tracking": "workspace:4.12.8", - "@ember-data/unpublished-test-infra": "workspace:4.12.8", - "@ember/optional-features": "^2.0.0", - "@ember/string": "^4.0.0", - "@ember/test-helpers": "^2.9.3", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.6.1", - "ember-cli": "~4.11.0", - "ember-cli-app-version": "^6.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-export-application-global": "^2.0.1", - "ember-inflector": "^4.0.2", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-qunit": "^6.2.0", - "ember-resolver": "^10.0.0", - "ember-source": "~4.12.0", - "loader.js": "^4.7.0", - "qunit": "^2.19.4", - "qunit-dom": "^2.0.0", - "webpack": "^5.77.0" - }, - "engines": { - "node": "^14.8.0 || 16.* || >= 18.*" - }, - "ember": { - "edition": "octane" - }, - "volta": { - "extends": "../../package.json" - }, - "packageManager": "pnpm@9.7.1" -} diff --git a/tests/serializer-encapsulation/public/robots.txt b/tests/serializer-encapsulation/public/robots.txt deleted file mode 100644 index f5916452e5f..00000000000 --- a/tests/serializer-encapsulation/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# http://www.robotstxt.org -User-agent: * -Disallow: diff --git a/tests/serializer-encapsulation/testem.js b/tests/serializer-encapsulation/testem.js deleted file mode 100644 index e10b064501a..00000000000 --- a/tests/serializer-encapsulation/testem.js +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-require -const customDotReporter = require('@ember-data/unpublished-test-infra/src/testem/custom-dot-reporter'); - -// eslint-disable-next-line no-console -console.log(`\n\nLaunching with ${process.env.TESTEM_CI_LAUNCHER || 'Chrome'}\n\n`); - -module.exports = { - test_page: 'tests/index.html?hidepassed&nocontainer', - disable_watching: true, - reporter: customDotReporter, - launch_in_ci: [process.env.TESTEM_CI_LAUNCHER || 'Chrome'], - launch_in_dev: ['Chrome'], - browser_start_timeout: 120, - browser_args: { - Chrome: { - ci: [ - '--headless', - '--disable-dev-shm-usage', - '--disable-software-rasterizer', - '--mute-audio', - '--remote-debugging-port=0', - '--window-size=1440,900', - '--no-sandbox', - ], - }, - }, - Firefox: { - ci: ['-headless', '-width 1440', '-height 900'], - }, -}; diff --git a/tests/serializer-encapsulation/tests/helpers/.gitkeep b/tests/serializer-encapsulation/tests/helpers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/tests/index.html b/tests/serializer-encapsulation/tests/index.html deleted file mode 100644 index 180b356ee61..00000000000 --- a/tests/serializer-encapsulation/tests/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - EncapsulationTestApp Tests - - - - {{content-for "head"}} - {{content-for "test-head"}} - - - - - - {{content-for "head-footer"}} - {{content-for "test-head-footer"}} - - - {{content-for "body"}} - {{content-for "test-body"}} - -
          -
          -
          -
          -
          -
          - - - - - - - - {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - - - diff --git a/tests/serializer-encapsulation/tests/integration/.gitkeep b/tests/serializer-encapsulation/tests/integration/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/tests/integration/smoke-test.js b/tests/serializer-encapsulation/tests/integration/smoke-test.js deleted file mode 100644 index f6e86e21aa6..00000000000 --- a/tests/serializer-encapsulation/tests/integration/smoke-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* global require */ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -function assertPackageNotPresent(packageName, assert) { - const entries = Object.keys(require.entries); - const entriesFromPackage = entries.filter((m) => m.indexOf(packageName) === 0); - const importedDependencies = {}; - const entriesImportingPackage = entries.filter((m) => { - const deps = require.entries[m].deps; - const moduleDeps = deps.filter((d) => d.indexOf(packageName) === 0); - - if (moduleDeps.length) { - importedDependencies[m] = moduleDeps; - } - return moduleDeps.length > 0; - }); - - assert.ok(entries.length > 0, 'We have modules'); - assert.ok( - entriesFromPackage.length === 0, - `We expect no modules from ${packageName} ${ - entriesFromPackage.length > 0 ? `found: [\n\t"${entriesFromPackage.join('",\n\t"')}"\n]` : '' - }` - ); - assert.ok( - entriesImportingPackage.length === 0, - `We expect no modules with dependencies on ${packageName} ${ - entriesImportingPackage.length > 0 ? `found:\n${JSON.stringify(importedDependencies, null, 2)}` : '' - }` - ); -} - -module('Serializer Contract | Smoke Tests', function (hooks) { - setupTest(hooks); - - test('No @ember-data/serializer modules are present', function (assert) { - assertPackageNotPresent('@ember-data/serializer', assert); - }); - - test('No ember-data modules are present', function (assert) { - assertPackageNotPresent('ember-data', assert); - }); -}); diff --git a/tests/serializer-encapsulation/tests/test-helper.js b/tests/serializer-encapsulation/tests/test-helper.js deleted file mode 100644 index a16f69329b5..00000000000 --- a/tests/serializer-encapsulation/tests/test-helper.js +++ /dev/null @@ -1,23 +0,0 @@ -import { setApplication } from '@ember/test-helpers'; - -import * as QUnit from 'qunit'; -import { setup } from 'qunit-dom'; - -import { start } from 'ember-qunit'; - -import assertAllDeprecations from '@ember-data/unpublished-test-infra/test-support/assert-all-deprecations'; -import configureAsserts from '@ember-data/unpublished-test-infra/test-support/qunit-asserts'; - -import Application from '../app'; -import config from '../config/environment'; - -setup(QUnit.assert); - -configureAsserts(); - -setApplication(Application.create(config.APP)); - -assertAllDeprecations(); - -QUnit.config.testTimeout = 2000; -start({ setupTestIsolationValidation: true }); diff --git a/tests/serializer-encapsulation/tests/unit/.gitkeep b/tests/serializer-encapsulation/tests/unit/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/serializer-encapsulation/vendor/.gitkeep b/tests/serializer-encapsulation/vendor/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/vite-basic-compat/.editorconfig b/tests/vite-basic-compat/.editorconfig new file mode 100644 index 00000000000..c35a002406b --- /dev/null +++ b/tests/vite-basic-compat/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/tests/vite-basic-compat/.ember-cli b/tests/vite-basic-compat/.ember-cli new file mode 100644 index 00000000000..4defd284ec1 --- /dev/null +++ b/tests/vite-basic-compat/.ember-cli @@ -0,0 +1,7 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": true +} diff --git a/tests/vite-basic-compat/.gitignore b/tests/vite-basic-compat/.gitignore new file mode 100644 index 00000000000..71ad79d02ea --- /dev/null +++ b/tests/vite-basic-compat/.gitignore @@ -0,0 +1,25 @@ +# compiled output +/dist/ +/declarations/ + +# dependencies +/node_modules/ + +# misc +/.env* +/.pnp* +/.eslintcache +/coverage/ +/npm-debug.log* +/testem.log +/yarn-error.log + +# ember-try +/.node_modules.ember-try/ +/npm-shrinkwrap.json.ember-try +/package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try + +# broccoli-debug +/DEBUG/ diff --git a/tests/adapter-encapsulation/.template-lintrc.js b/tests/vite-basic-compat/.template-lintrc.js similarity index 100% rename from tests/adapter-encapsulation/.template-lintrc.js rename to tests/vite-basic-compat/.template-lintrc.js diff --git a/tests/vite-basic-compat/README.md b/tests/vite-basic-compat/README.md new file mode 100644 index 00000000000..ff45966fca2 --- /dev/null +++ b/tests/vite-basic-compat/README.md @@ -0,0 +1,57 @@ +# vite-basic-compat + +This README outlines the details of collaborating on this Ember application. +A short introduction of this app could easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) +- [pnpm](https://pnpm.io/) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd vite-basic-compat` +- `pnpm install` + +## Running / Development + +- `pnpm start` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `pnpm test` +- `pnpm test:ember --server` + +### Linting + +- `pnpm lint` +- `pnpm lint:fix` + +### Building + +- `pnpm ember build` (development) +- `pnpm build` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/tests/vite-basic-compat/app/adapters/application.ts b/tests/vite-basic-compat/app/adapters/application.ts new file mode 100644 index 00000000000..091f5ab1225 --- /dev/null +++ b/tests/vite-basic-compat/app/adapters/application.ts @@ -0,0 +1,14 @@ +import RESTAdapter from '@ember-data/adapter/rest'; +import type { SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; + +export default class ApplicationAdapter extends RESTAdapter { + namespace = 'api'; + + urlForFindAll(type: string, snapshots: SnapshotRecordArray) { + let url = super.urlForFindAll(type, snapshots); + if (url.endsWith('/')) { + url = url.substring(0, url.length - 2); + } + return url + '.json'; + } +} diff --git a/tests/vite-basic-compat/app/app.ts b/tests/vite-basic-compat/app/app.ts new file mode 100644 index 00000000000..a3c0402fce4 --- /dev/null +++ b/tests/vite-basic-compat/app/app.ts @@ -0,0 +1,13 @@ +import Application from '@ember/application'; +import compatModules from '@embroider/virtual/compat-modules'; +import Resolver from 'ember-resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver.withModules(compatModules); +} + +loadInitializers(App, config.modulePrefix, compatModules); diff --git a/tests/debug-encapsulation/app/helpers/.gitkeep b/tests/vite-basic-compat/app/components/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/helpers/.gitkeep rename to tests/vite-basic-compat/app/components/.gitkeep diff --git a/tests/vite-basic-compat/app/config/environment.d.ts b/tests/vite-basic-compat/app/config/environment.d.ts new file mode 100644 index 00000000000..1b9d86cc01d --- /dev/null +++ b/tests/vite-basic-compat/app/config/environment.d.ts @@ -0,0 +1,14 @@ +/** + * Type declarations for + * import config from 'vite-basic-compat/config/environment' + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/tests/vite-basic-compat/app/config/environment.js b/tests/vite-basic-compat/app/config/environment.js new file mode 100644 index 00000000000..7da789a8256 --- /dev/null +++ b/tests/vite-basic-compat/app/config/environment.js @@ -0,0 +1,3 @@ +import loadConfigFromMeta from '@embroider/config-meta-loader'; + +export default loadConfigFromMeta('vite-basic-compat'); diff --git a/tests/debug-encapsulation/app/models/.gitkeep b/tests/vite-basic-compat/app/controllers/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/models/.gitkeep rename to tests/vite-basic-compat/app/controllers/.gitkeep diff --git a/tests/debug-encapsulation/app/routes/.gitkeep b/tests/vite-basic-compat/app/helpers/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/routes/.gitkeep rename to tests/vite-basic-compat/app/helpers/.gitkeep diff --git a/tests/vite-basic-compat/app/models/user.ts b/tests/vite-basic-compat/app/models/user.ts new file mode 100644 index 00000000000..33d0be8ad43 --- /dev/null +++ b/tests/vite-basic-compat/app/models/user.ts @@ -0,0 +1,5 @@ +import Model, { attr } from '@ember-data/model'; + +export default class User extends Model { + @attr declare name: string; +} diff --git a/tests/vite-basic-compat/app/router.ts b/tests/vite-basic-compat/app/router.ts new file mode 100644 index 00000000000..72e351042a6 --- /dev/null +++ b/tests/vite-basic-compat/app/router.ts @@ -0,0 +1,11 @@ +import EmberRouter from '@ember/routing/router'; +import config from 'vite-basic-compat/config/environment'; + +export default class Router extends EmberRouter { + location = config.locationType; + rootURL = config.rootURL; +} + +Router.map(function () { + // Add route declarations here +}); diff --git a/tests/debug-encapsulation/app/templates/components/.gitkeep b/tests/vite-basic-compat/app/routes/.gitkeep similarity index 100% rename from tests/debug-encapsulation/app/templates/components/.gitkeep rename to tests/vite-basic-compat/app/routes/.gitkeep diff --git a/tests/vite-basic-compat/app/styles/app.css b/tests/vite-basic-compat/app/styles/app.css new file mode 100644 index 00000000000..2763afa4cfa --- /dev/null +++ b/tests/vite-basic-compat/app/styles/app.css @@ -0,0 +1 @@ +/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ diff --git a/tests/vite-basic-compat/app/templates/application.gts b/tests/vite-basic-compat/app/templates/application.gts new file mode 100644 index 00000000000..3a383ff2beb --- /dev/null +++ b/tests/vite-basic-compat/app/templates/application.gts @@ -0,0 +1,10 @@ +import Route from 'ember-route-template'; +import { pageTitle } from 'ember-page-title'; + +export default Route( + +); diff --git a/tests/vite-basic-compat/babel.config.cjs b/tests/vite-basic-compat/babel.config.cjs new file mode 100644 index 00000000000..e3992fdbbb3 --- /dev/null +++ b/tests/vite-basic-compat/babel.config.cjs @@ -0,0 +1,47 @@ +const { babelCompatSupport, templateCompatSupport } = require('@embroider/compat/babel'); + +module.exports = { + plugins: [ + [ + '@babel/plugin-transform-typescript', + { + allExtensions: true, + onlyRemoveTypeImports: true, + allowDeclareFields: true, + }, + ], + [ + 'babel-plugin-ember-template-compilation', + { + compilerPath: 'ember-source/dist/ember-template-compiler.js', + enableLegacyModules: [ + 'ember-cli-htmlbars', + 'ember-cli-htmlbars-inline-precompile', + 'htmlbars-inline-precompile', + ], + transforms: [...templateCompatSupport()], + }, + ], + [ + 'module:decorator-transforms', + { + runtime: { + import: require.resolve('decorator-transforms/runtime-esm'), + }, + }, + ], + [ + '@babel/plugin-transform-runtime', + { + absoluteRuntime: __dirname, + useESModules: true, + regenerator: false, + }, + ], + ...babelCompatSupport(), + ], + + generatorOpts: { + compact: false, + }, +}; diff --git a/tests/vite-basic-compat/config/ember-cli-update.json b/tests/vite-basic-compat/config/ember-cli-update.json new file mode 100644 index 00000000000..899f637ce16 --- /dev/null +++ b/tests/vite-basic-compat/config/ember-cli-update.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "@embroider/app-blueprint", + "version": "0.14.0", + "blueprints": [ + { + "name": "@embroider/app-blueprint", + "isBaseBlueprint": true, + "options": ["--package-manager pnpm"] + } + ] + } + ] +} diff --git a/tests/vite-basic-compat/config/environment.js b/tests/vite-basic-compat/config/environment.js new file mode 100644 index 00000000000..9d512a9873f --- /dev/null +++ b/tests/vite-basic-compat/config/environment.js @@ -0,0 +1,48 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'vite-basic-compat', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + EXTEND_PROTOTYPES: false, + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/vite-basic-compat/config/optional-features.json b/tests/vite-basic-compat/config/optional-features.json new file mode 100644 index 00000000000..5329dd9913b --- /dev/null +++ b/tests/vite-basic-compat/config/optional-features.json @@ -0,0 +1,7 @@ +{ + "application-template-wrapper": false, + "default-async-observers": true, + "jquery-integration": false, + "template-only-glimmer-components": true, + "no-implicit-route-model": true +} diff --git a/tests/vite-basic-compat/config/targets.js b/tests/vite-basic-compat/config/targets.js new file mode 100644 index 00000000000..9f6cc639666 --- /dev/null +++ b/tests/vite-basic-compat/config/targets.js @@ -0,0 +1,7 @@ +'use strict'; + +const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; + +module.exports = { + browsers, +}; diff --git a/tests/vite-basic-compat/ember-cli-build.js b/tests/vite-basic-compat/ember-cli-build.js new file mode 100644 index 00000000000..ebb76e53a57 --- /dev/null +++ b/tests/vite-basic-compat/ember-cli-build.js @@ -0,0 +1,10 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const { maybeEmbroider } = require('@embroider/test-setup'); + +module.exports = function (defaults) { + let app = new EmberApp(defaults, {}); + + return maybeEmbroider(app); +}; diff --git a/tests/vite-basic-compat/eslint.config.mjs b/tests/vite-basic-compat/eslint.config.mjs new file mode 100644 index 00000000000..1d77232e27e --- /dev/null +++ b/tests/vite-basic-compat/eslint.config.mjs @@ -0,0 +1,135 @@ +import globals from 'globals'; +import js from '@eslint/js'; + +import ts from 'typescript-eslint'; + +import ember from 'eslint-plugin-ember'; +import emberRecommended from 'eslint-plugin-ember/configs/recommended'; +import gjsRecommended from 'eslint-plugin-ember/configs/recommended-gjs'; +import gtsRecommended from 'eslint-plugin-ember/configs/recommended-gts'; + +import prettier from 'eslint-plugin-prettier/recommended'; +import qunit from 'eslint-plugin-qunit'; +import n from 'eslint-plugin-n'; + +import emberParser from 'ember-eslint-parser'; +import babelParser from '@babel/eslint-parser'; + +const parserOptions = { + esm: { + js: { + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + }, + ts: { + /* don't type check this package */ + ecmaFeatures: { modules: true }, + ecmaVersion: 'latest', + }, + }, +}; + +export default ts.config( + js.configs.recommended, + prettier, + { + files: ['**/*.js'], + languageOptions: { + parser: babelParser, + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + plugins: { + ember, + }, + rules: { + ...emberRecommended.rules, + }, + }, + { + files: ['**/*.gjs'], + languageOptions: { + parser: emberParser, + parserOptions: parserOptions.esm.js, + globals: { + ...globals.browser, + }, + }, + plugins: { + ember, + }, + rules: { + ...emberRecommended.rules, + ...gjsRecommended.rules, + }, + }, + { + files: ['**/*.{ts,gts}'], + plugins: { ember }, + languageOptions: { + parserOptions: parserOptions.esm.ts, + }, + extends: [...ts.configs.recommended, ...emberRecommended, ...gtsRecommended], + }, + { + files: ['tests/**/*-test.{js,gjs}'], + plugins: { + qunit, + }, + }, + /** + * CJS node files + */ + { + files: [ + '**/*.cjs', + 'config/**/*.js', + 'testem.js', + 'testem*.js', + '.prettierrc.js', + '.stylelintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + ], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'script', + ecmaVersion: 'latest', + globals: { + ...globals.node, + }, + }, + }, + /** + * ESM node files + */ + { + files: ['*.mjs'], + plugins: { + n, + }, + + languageOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + parserOptions: parserOptions.esm.js, + globals: { + ...globals.node, + }, + }, + }, + /** + * Settings + */ + { + ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + } +); diff --git a/tests/vite-basic-compat/index.html b/tests/vite-basic-compat/index.html new file mode 100644 index 00000000000..d429522580f --- /dev/null +++ b/tests/vite-basic-compat/index.html @@ -0,0 +1,29 @@ + + + + + AppTemplate + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/vite-basic-compat/package.json b/tests/vite-basic-compat/package.json new file mode 100644 index 00000000000..140bd1e2109 --- /dev/null +++ b/tests/vite-basic-compat/package.json @@ -0,0 +1,164 @@ +{ + "name": "vite-basic-compat", + "version": "4.12.8", + "private": true, + "description": "Small description for vite-basic-compat goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "vite build", + "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"", + "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "vite", + "test:vite": "vite build --mode test && ember test --path dist", + "sync-hardlinks": "bun run sync-dependencies-meta-injected" + }, + "dependenciesMeta": { + "ember-data": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/adapter": { + "injected": true + }, + "@ember-data/graph": { + "injected": true + }, + "@ember-data/debug": { + "injected": true + }, + "@ember-data/model": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/request-utils": { + "injected": true + }, + "@ember-data/legacy-compat": { + "injected": true + }, + "@ember-data/serializer": { + "injected": true + }, + "@ember-data/unpublished-test-infra": { + "injected": true + }, + "@warp-drive/core-types": { + "injected": true + }, + "@warp-drive/build-config": { + "injected": true + } + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/eslint-parser": "^7.25.9", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/plugin-transform-typescript": "^7.25.9", + "@babel/runtime": "^7.26.0", + "@ember-data/adapter": "workspace:*", + "@ember-data/debug": "workspace:*", + "@ember-data/graph": "workspace:*", + "@ember-data/json-api": "workspace:*", + "@ember-data/legacy-compat": "workspace:*", + "@ember-data/model": "workspace:*", + "@ember-data/request": "workspace:*", + "@ember-data/request-utils": "workspace:*", + "@ember-data/serializer": "workspace:*", + "@ember-data/store": "workspace:*", + "@ember-data/tracking": "workspace:*", + "@ember-data/unpublished-test-infra": "workspace:*", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^4.0.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", + "@embroider/core": "3.4.20-unstable.4070ba7", + "@embroider/test-setup": "4.0.1-unstable.4070ba7", + "@embroider/vite": "0.2.2-unstable.4070ba7", + "@glimmer/component": "^1.1.2", + "@glimmer/tracking": "^1.1.2", + "@rollup/plugin-babel": "^6.0.4", + "@tsconfig/ember": "^3.0.8", + "@types/eslint__js": "^8.42.3", + "@types/qunit": "2.19.10", + "@types/rsvp": "^4.0.9", + "@typescript-eslint/eslint-plugin": "^8.14.0", + "@typescript-eslint/parser": "^8.14.0", + "@warp-drive/build-config": "workspace:*", + "@warp-drive/core-types": "workspace:*", + "@warp-drive/internal-config": "workspace:*", + "babel-plugin-ember-template-compilation": "^2.3.0", + "concurrently": "^9.1.0", + "decorator-transforms": "^2.3.0", + "ember-auto-import": "^2.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-htmlbars": "^6.3.0", + "ember-data": "workspace:*", + "ember-load-initializers": "^3.0.1", + "ember-modifier": "^4.2.0", + "ember-page-title": "^8.2.3", + "ember-qunit": "9.0.1", + "ember-resolver": "^13.1.0", + "ember-route-template": "^1.0.3", + "ember-source": "~5.12.0", + "ember-template-lint": "^6.0.0", + "ember-welcome-page": "^7.0.2", + "eslint": "^9.14.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-ember": "^12.3.1", + "eslint-plugin-n": "^17.13.1", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-qunit": "^8.1.2", + "globals": "^15.12.0", + "loader.js": "^4.7.0", + "pnpm-sync-dependencies-meta-injected": "0.0.14", + "prettier": "^3.3.3", + "prettier-plugin-ember-template-tag": "^2.0.4", + "qunit": "^2.22.0", + "qunit-dom": "^3.3.0", + "tracked-built-ins": "^3.3.0", + "typescript": "^5.5.4", + "typescript-eslint": "^8.13.0", + "vite": "^5.4.11", + "webpack": "^5.95.0" + }, + "engines": { + "node": ">= 18" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + }, + "ember-addon": { + "type": "app", + "version": 2 + }, + "exports": { + "./tests/*": "./tests/*", + "./*": "./app/*" + } +} diff --git a/tests/debug-encapsulation/public/robots.txt b/tests/vite-basic-compat/public/robots.txt similarity index 100% rename from tests/debug-encapsulation/public/robots.txt rename to tests/vite-basic-compat/public/robots.txt diff --git a/tests/vite-basic-compat/testem.js b/tests/vite-basic-compat/testem.js new file mode 100644 index 00000000000..b4b6691fded --- /dev/null +++ b/tests/vite-basic-compat/testem.js @@ -0,0 +1,25 @@ +'use strict'; + +if (typeof module !== 'undefined') { + module.exports = { + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], + browser_start_timeout: 120, + browser_args: { + Chrome: { + ci: [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900', + ].filter(Boolean), + }, + }, + }; +} diff --git a/tests/vite-basic-compat/tests/acceptance/visit-test.js b/tests/vite-basic-compat/tests/acceptance/visit-test.js new file mode 100644 index 00000000000..f09ab7273ed --- /dev/null +++ b/tests/vite-basic-compat/tests/acceptance/visit-test.js @@ -0,0 +1,23 @@ +import { visit } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import { setupApplicationTest } from 'ember-qunit'; + +module('it works', function (hooks) { + setupApplicationTest(hooks); + + test('we can boot the app', async function (assert) { + await visit('/'); + assert.ok('it works!'); + }); + + test('we can use the store', async function (assert) { + const { owner } = this; + const store = owner.lookup('service:store'); + + const record = store.createRecord('user', { name: 'Chris' }); + + assert.strictEqual(record.name, 'Chris', 'correct name'); + }); +}); diff --git a/tests/vite-basic-compat/tests/helpers/index.ts b/tests/vite-basic-compat/tests/helpers/index.ts new file mode 100644 index 00000000000..e190f567eda --- /dev/null +++ b/tests/vite-basic-compat/tests/helpers/index.ts @@ -0,0 +1,43 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, + type SetupTestOptions, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +export { setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/vite-basic-compat/tests/index.html b/tests/vite-basic-compat/tests/index.html new file mode 100644 index 00000000000..c1095896893 --- /dev/null +++ b/tests/vite-basic-compat/tests/index.html @@ -0,0 +1,40 @@ + + + + + AppTemplate Tests + + + + {{content-for "head"}} {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} {{content-for "test-head-footer"}} + + + {{content-for "body"}} {{content-for "test-body"}} + +
          +
          +
          +
          +
          +
          + + + + + + + + + {{content-for "body-footer"}} + + diff --git a/tests/debug-encapsulation/tests/helpers/.gitkeep b/tests/vite-basic-compat/tests/integration/.gitkeep similarity index 100% rename from tests/debug-encapsulation/tests/helpers/.gitkeep rename to tests/vite-basic-compat/tests/integration/.gitkeep diff --git a/tests/vite-basic-compat/tests/test-helper.ts b/tests/vite-basic-compat/tests/test-helper.ts new file mode 100644 index 00000000000..714baa96bb7 --- /dev/null +++ b/tests/vite-basic-compat/tests/test-helper.ts @@ -0,0 +1,14 @@ +import Application from 'vite-basic-compat/app'; +import config from 'vite-basic-compat/config/environment'; +import * as QUnit from 'qunit'; +import { setApplication } from '@ember/test-helpers'; +import { setup } from 'qunit-dom'; +import { start as qunitStart } from 'ember-qunit'; + +export function start() { + setApplication(Application.create(config.APP)); + + setup(QUnit.assert); + + qunitStart(); +} diff --git a/tests/debug-encapsulation/tests/integration/.gitkeep b/tests/vite-basic-compat/tests/unit/.gitkeep similarity index 100% rename from tests/debug-encapsulation/tests/integration/.gitkeep rename to tests/vite-basic-compat/tests/unit/.gitkeep diff --git a/tests/vite-basic-compat/tsconfig.json b/tests/vite-basic-compat/tsconfig.json new file mode 100644 index 00000000000..acb0791a852 --- /dev/null +++ b/tests/vite-basic-compat/tsconfig.json @@ -0,0 +1,107 @@ +{ + "include": ["app/**/*", "config/**/*", "tests/**/*"], + "compilerOptions": { + "lib": ["DOM", "ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "downlevelIteration": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "baseUrl": ".", + "noImplicitOverride": false, + "experimentalDecorators": true, + "incremental": true, + "noEmit": true, + "declaration": false, + "types": ["ember-source/types", "@embroider/core/virtual"], + "paths": { + "vite-basic-compat/*": ["./app/*"], + "vite-basic-compat/tests/*": ["./tess/*"], + "@ember-data/unpublished-test-infra": ["../../packages/unpublished-test-infra/unstable-preview-types"], + "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/unstable-preview-types/*"], + "ember-data": ["../../packages/-ember-data/unstable-preview-types"], + "ember-data/*": ["../../packages/-ember-data/unstable-preview-types/*"], + "@ember-data/request": ["../../packages/request/unstable-preview-types"], + "@ember-data/request/*": ["../../packages/request/unstable-preview-types/*"], + "@ember-data/store": ["../../packages/store/unstable-preview-types"], + "@ember-data/store/*": ["../../packages/store/unstable-preview-types/*"], + "@ember-data/adapter": ["../../packages/adapter/unstable-preview-types"], + "@ember-data/adapter/*": ["../../packages/adapter/unstable-preview-types/*"], + "@ember-data/debug": ["../../packages/debug/unstable-preview-types"], + "@ember-data/debug/*": ["../../packages/debug/unstable-preview-types/*"], + "@ember-data/graph": ["../../packages/graph/unstable-preview-types"], + "@ember-data/graph/*": ["../../packages/graph/unstable-preview-types/*"], + "@ember-data/json-api": ["../../packages/json-api/unstable-preview-types"], + "@ember-data/json-api/*": ["../../packages/json-api/unstable-preview-types/*"], + "@ember-data/legacy-compat": ["../../packages/legacy-compat/unstable-preview-types"], + "@ember-data/legacy-compat/*": ["../../packages/legacy-compat/unstable-preview-types/*"], + "@ember-data/model": ["../../packages/model/unstable-preview-types"], + "@ember-data/model/*": ["../../packages/model/unstable-preview-types/*"], + "@ember-data/request-utils": ["../../packages/request-utils/unstable-preview-types"], + "@ember-data/request-utils/*": ["../../packages/request-utils/unstable-preview-types/*"], + "@ember-data/serializer": ["../../packages/serializer/unstable-preview-types"], + "@ember-data/serializer/*": ["../../packages/serializer/unstable-preview-types/*"], + "@ember-data/tracking": ["../../packages/tracking/unstable-preview-types"], + "@ember-data/tracking/*": ["../../packages/tracking/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/build-config": ["../../packages/build-config/unstable-preview-types"], + "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], + "*": ["./types/*"] + } + }, + "references": [ + { + "path": "../../packages/unpublished-test-infra" + }, + { + "path": "../../packages/-ember-data" + }, + { + "path": "../../packages/request" + }, + { + "path": "../../packages/store" + }, + { + "path": "../../packages/adapter" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/graph" + }, + { + "path": "../../packages/json-api" + }, + { + "path": "../../packages/legacy-compat" + }, + { + "path": "../../packages/model" + }, + { + "path": "../../packages/request-utils" + }, + { + "path": "../../packages/serializer" + }, + { + "path": "../../packages/tracking" + }, + { + "path": "../../packages/core-types" + }, + { + "path": "../../packages/build-config" + } + ] +} diff --git a/tests/vite-basic-compat/types/ember-data/types/registries/model.d.ts b/tests/vite-basic-compat/types/ember-data/types/registries/model.d.ts new file mode 100644 index 00000000000..bdd8c0f178d --- /dev/null +++ b/tests/vite-basic-compat/types/ember-data/types/registries/model.d.ts @@ -0,0 +1,7 @@ +/** + * Catch-all for ember-data. + */ +export default interface ModelRegistry { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} diff --git a/tests/vite-basic-compat/types/index.d.ts b/tests/vite-basic-compat/types/index.d.ts new file mode 100644 index 00000000000..217abbb25c6 --- /dev/null +++ b/tests/vite-basic-compat/types/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/vite-basic-compat/vite.config.mjs b/tests/vite-basic-compat/vite.config.mjs new file mode 100644 index 00000000000..219253dbea9 --- /dev/null +++ b/tests/vite-basic-compat/vite.config.mjs @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import { extensions, classicEmberSupport, ember } from '@embroider/vite'; +import { babel } from '@rollup/plugin-babel'; + +export default defineConfig({ + plugins: [ + classicEmberSupport(), + ember(), + // extra plugins here + babel({ + babelHelpers: 'runtime', + extensions, + }), + ], +}); diff --git a/tsconfig.json b/tsconfig.json index 20936a56f65..f102572daee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,106 +1,28 @@ { - "extends": "./tsconfig.root.json", - "compilerOptions": { - "experimentalDecorators": true, - "noImplicitAny": false - }, - "files": [ - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/utils/is-thenable.ts", - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/index.ts", - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/check-matcher.ts", - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-warning.ts", - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-deprecation.ts", - "packages/unpublished-test-infra/addon-test-support/qunit-asserts/assert-assertion.ts", - "tests/fastboot/types/global.d.ts", - "tests/fastboot/types/fastboot-test-app/index.d.ts", - "tests/fastboot/app/serializers/application.ts", - "tests/fastboot/app/router.ts", - "tests/fastboot/app/resolver.ts", - "tests/fastboot/app/config/environment.d.ts", - "tests/fastboot/app/app.ts", - "tests/fastboot/app/adapters/application.ts", - "packages/store/src/index.ts", - "packages/store/src/-private/utils/is-non-empty-string.ts", - "packages/store/src/-private/utils/construct-resource.ts", - "ember-data-types/q/utils.ts", - "ember-data-types/q/schema-service.ts", - "ember-data-types/q/record-instance.ts", - "ember-data-types/q/cache-store-wrapper.ts", - "ember-data-types/q/record-data-schemas.ts", - "ember-data-types/q/record-data-json-api.ts", - "ember-data-types/q/promise-proxies.ts", - "ember-data-types/q/minimum-serializer-interface.ts", - "ember-data-types/q/minimum-adapter-interface.ts", - "ember-data-types/q/identifier.ts", - "ember-data-types/q/fetch-manager.ts", - "ember-data-types/q/ember-data-json-api.ts", - "ember-data-types/q/ds-model.ts", - "packages/store/src/-private/managers/cache-store-wrapper.ts", - "packages/store/src/-private/legacy-model-support/schema-definition-service.ts", - "packages/store/src/-private/network/request-cache.ts", - "packages/store/src/-private/managers/notification-manager.ts", - "packages/store/src/-private/caches/cache-utils.ts", - "packages/store/src/-private/utils/normalize-model-name.ts", - "packages/store/src/-private/legacy-model-support/shim-model-class.ts", - "packages/store/src/-private/store-service.ts", - "packages/store/src/-private/utils/coerce-id.ts", - "packages/store/src/-private/index.ts", - "packages/store/src/-private/caches/identifier-cache.ts", - "packages/serializer/src/index.ts", - "tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts", - "tests/graph/tests/integration/graph/graph-test.ts", - "tests/graph/tests/integration/graph/operations-test.ts", - "tests/graph/tests/integration/graph/edge-test.ts", - "tests/graph/tests/integration/graph/edge-removal/setup.ts", - "tests/graph/tests/integration/graph/edge-removal/helpers.ts", - "tests/graph/tests/integration/graph/edge-removal/abstract-edge-removal-test.ts", - "tests/graph/tests/integration/graph.ts", - "packages/graph/src/-private/relationships/state/has-many.ts", - "packages/graph/src/-private/relationships/state/belongs-to.ts", - "packages/graph/src/-private/normalize-link.ts", - "packages/graph/src/-private/graph/operations/update-relationship.ts", - "packages/graph/src/-private/graph/operations/replace-related-records.ts", - "packages/graph/src/-private/graph/operations/replace-related-record.ts", - "packages/graph/src/-private/graph/operations/remove-from-related-records.ts", - "packages/graph/src/-private/graph/operations/add-to-related-records.ts", - "packages/graph/src/-private/graph/index.ts", - "packages/graph/src/-private/graph/-utils.ts", - "packages/graph/src/-private/graph/-state.ts", - "packages/graph/src/-private/graph/-operations.ts", - "packages/graph/src/-private/graph/-edge-definition.ts", - "packages/graph/src/-private/coerce-id.ts", - "packages/json-api/src/-private/cache.ts", - "packages/model/src/index.ts", - "packages/model/src/-private/util.ts", - "packages/model/src/-private/relationship-meta.ts", - "packages/model/src/-private/promise-many-array.ts", - "packages/model/src/-private/model-for-mixin.ts", - "packages/model/src/-private/record-state.ts", - "packages/model/src/-private/notify-changes.ts", - "packages/adapter/src/rest.ts", - "packages/adapter/src/json-api.ts", - "packages/adapter/src/index.ts", - "packages/adapter/src/-private/utils/serialize-query-params.ts", - "packages/adapter/src/-private/utils/fetch.ts", - "packages/adapter/src/-private/utils/determine-body-promise.ts", - "packages/adapter/src/-private/utils/continue-on-reject.ts", - "packages/adapter/src/-private/fastboot-interface.ts", - "packages/adapter/src/-private/build-url-mixin.ts", - "packages/-ember-data/addon/store.ts", - "tests/main/tests/unit/custom-class-support/custom-class-model-test.ts", - "tests/main/tests/integration/request-state-service-test.ts", - "tests/main/tests/integration/record-data/store-wrapper-test.ts", - "tests/main/tests/integration/record-data/record-data-test.ts", - "tests/main/tests/integration/record-data/record-data-state-test.ts", - "tests/main/tests/integration/record-data/record-data-errors-test.ts", - "tests/main/tests/integration/model-errors-test.ts", - "tests/main/tests/integration/identifiers/scenarios-test.ts", - "tests/main/tests/integration/identifiers/record-identifier-for-test.ts", - "tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts", - "tests/main/tests/integration/identifiers/new-records-test.ts", - "tests/main/tests/integration/identifiers/lid-reflection-test.ts", - "tests/main/tests/integration/identifiers/configuration-test.ts", - "tests/main/tests/integration/identifiers/cache-test.ts", - "tests/main/tests/helpers/accessors.ts" - ] - } + "glint": { + "environment": ["ember-loose", "ember-template-imports"] + }, + "files": [], + "include": [], + "references": [ + { "path": "./packages/-ember-data" }, + { "path": "./packages/active-record" }, + { "path": "./packages/adapter" }, + { "path": "./packages/build-config" }, + { "path": "./packages/core-types" }, + { "path": "./packages/debug" }, + { "path": "./packages/diagnostic" }, + { "path": "./packages/graph" }, + { "path": "./packages/holodeck" }, + { "path": "./packages/json-api" }, + { "path": "./packages/legacy-compat" }, + { "path": "./packages/model" }, + { "path": "./packages/request" }, + { "path": "./packages/request-utils" }, + { "path": "./packages/rest" }, + { "path": "./packages/serializer" }, + { "path": "./packages/store" }, + { "path": "./packages/tracking" }, + { "path": "./packages/unpublished-test-infra" } + ] +} diff --git a/tsconfig.root.json b/tsconfig.root.json deleted file mode 100644 index 607e182a5de..00000000000 --- a/tsconfig.root.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2020", - "moduleResolution": "node", - - // Enable faster builds - "incremental": true, - - "allowJs": false, - "checkJs": false, - - "alwaysStrict": true, - "strict": true, - "allowSyntheticDefaultImports": true, - - "noImplicitAny": true, - "noImplicitThis": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "strictPropertyInitialization": true, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "noEmitOnError": false, - "strictNullChecks": true, - "noErrorTruncation": true, - "preserveConstEnums": false, - "experimentalDecorators": true, - "pretty": true, - "noEmit": true, - "skipLibCheck": true, - - // Support generation of source maps. Note: you must *also* enable source - // maps in your `ember-cli-babel` config and/or `babel.config.js`. - "declaration": true, - "declarationMap": true, - "inlineSourceMap": true, - "inlineSources": true, - - "baseUrl": ".", - "paths": { - "ember-data": ["packages/-ember-data/addon"], - "ember-data/*": ["packages/-ember-data/addon/*"], - "@ember-data/types": ["ember-data-types"], - "@ember-data/types/*": ["ember-data-types/*"], - "@ember-data/store": ["packages/store/src"], - "@ember-data/store/*": ["packages/store/src/*"], - "@ember-data/tracking": ["packages/tracking/src"], - "@ember-data/tracking/*": ["packages/tracking/src/*"], - "@ember-data/request": ["packages/request/src"], - "@ember-data/request/*": ["packages/request/src/*"], - "@ember-data/request-utils": ["packages/request-utils/src"], - "@ember-data/request-utils/*": ["packages/request-utils/src/*"], - "@ember-data/experimental-preview-types": ["packages/experimental-preview-types/src"], - "@ember-data/experimental-preview-types/*": ["packages/experimental-preview-types/src/*"], - "@ember-data/debug": ["packages/debug/addon"], - "@ember-data/debug/*": ["packages/debug/addon/*"], - "@ember-data/model": ["packages/model/src"], - "@ember-data/model/*": ["packages/model/src/*"], - "@ember-data/graph": ["packages/graph/src"], - "@ember-data/graph/*": ["packages/graph/src/*"], - "@ember-data/adapter": ["packages/adapter/src"], - "@ember-data/adapter/*": ["packages/adapter/src/*"], - "@ember-data/adapter/error": ["packages/adapter/src/error"], - "@ember-data/serializer": ["packages/serializer/src"], - "@ember-data/serializer/*": ["packages/serializer/src/*"], - "ember-data/test-support": ["packages/-ember-data/addon-test-support"], - "ember-data/test-support/*": ["packages/-ember-data/addon-test-support/*"], - "@ember-data/json-api": ["packages/json-api/src"], - "@ember-data/json-api/*": ["packages/json-api/src/*"], - "@ember-data/legacy-compat": ["packages/legacy-compat/src"], - "@ember-data/legacy-compat/*": ["packages/legacy-compat/src/*"], - "@ember-data/canary-features": ["packages/private-build-infra/virtual-packages/canary-features.d.ts"], - "@ember-data/debugging": ["packages/private-build-infra/virtual-packages/debugging.d.ts"], - "@ember-data/deprecations": ["packages/private-build-infra/virtual-packages/deprecations.d.ts"], - "@ember-data/packages": ["packages/private-build-infra/virtual-packages/packages.d.ts"], - "@ember-data/env": ["packages/private-build-infra/virtual-packages/env.d.ts"], - "@ember-data/unpublished-test-infra/test-support": ["packages/unpublished-test-infra/addon-test-support"], - "@ember-data/unpublished-test-infra/test-support/*": ["packages/unpublished-test-infra/addon-test-support/*"], - "fastboot-test-app/tests/*": ["tests/*"], - "fastboot-test-app/*": ["app/*"], - "main-test-app/tests/*": ["tests/main/tests/*"], - "main-test-app/*": ["tests/main/app/*"], - "graph-test-app/tests/*": ["tests/graph/tests/*"], - "graph-test-app/*": ["tests/graph/app/*"], - "request-test-app/tests/*": ["tests/request/tests/*"], - "request-test-app/*": ["tests/request/app/*"], - "*": ["@types/*", "packages/fastboot-test-app/types/*"], - "ember-inflector": [ - "tests/main/node_modules/ember-inflector", - "packages/-ember-data/node_modules/ember-inflector" - ] - } - }, - "include": [ - "@types/**/*", - "ember-data-types/**/*", - "packages/**/app/**/*", - "packages/**/addon/**/*", - "packages/**/tests/**/*", - "packages/tracking/src/**/*", - "packages/request/src/**/*", - "packages/request-utils/src/**/*", - "packages/store/src/**/*", - "packages/adapter/src/**/*", - "packages/graph/src/**/*", - "packages/model/src/**/*", - "packages/legacy-compat/src/**/*", - "packages/json-api/src/**/*", - "packages/serializer/src/**/*", - "packages/experimental-preview-types/src/**/*", - "packages/fastboot-test-app/types/**/*", - "packages/private-build-infra/canary-features/**/*", - "packages/**/test-support/**/*", - "packages/**/addon-test-support/**/*", - "tests/**/app/**/*", - "tests/**/addon/**/*", - "tests/**/tests/**/*" - ], - "exclude": ["node_modules", "packages/**/node_modules", "packages/**/dist", "packages/**/DEBUG"] -} diff --git a/turbo.json b/turbo.json new file mode 100644 index 00000000000..6c41c76d4b2 --- /dev/null +++ b/turbo.json @@ -0,0 +1,193 @@ +{ + // Additive to package.json and turbo.json + // + // https://turbo.build/repo/docs/core-concepts/caching/file-inputs#specifying-additional-inputs + "globalDependencies": ["pnpm-lock.yaml", "patches/*", ".github/*"], + + "pipeline": { + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + // + // Initial Setup + // + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + "build:infra": { + "inputs": [ + // + V2-Addon convention + "src/**", + "cjs-src/**", + "addon-main.*", + "tsconfig.json", + "babel.*", + "vite.config-cjs.*", + "vite.config.*", + "../../config/**" + ], + "outputs": [ + // V1-Addon convention + "addon-test-support/**", + // V2-Addon convention + "dist/**", + "unstable-preview-types/**", + "tsconfig.tsbuildinfo" + ], + "dependsOn": [], + // https://turbo.build/repo/docs/reference/configuration#outputmode + "outputMode": "new-only" + }, + + // run build in all library packages + // these do not require any associated packages + // to have been built to build other than + // the build:infra packages + "build:pkg": { + "inputs": [ + // + V2-Addon convention + "src/**", + "addon-main.*", + "tsconfig.json", + "babel.*", + "vite.config.*", + "../../config/**" + ], + "outputs": [ + // V1-Addon convention + "addon-test-support/**", + // V2-Addon convention + "dist/**", + "unstable-preview-types/**", + "tsconfig.tsbuildinfo" + ], + "dependsOn": ["^build:infra", "^build:pkg"], + // https://turbo.build/repo/docs/reference/configuration#outputmode + "outputMode": "new-only" + }, + + // run build in all library packages + // that ship gts files + // these do not require any associated packages + // to have been built to build other than + // the build:infra packages + "build:glint": { + "inputs": [ + // + V2-Addon convention + "src/**", + "addon-main.*", + "tsconfig.json", + "babel.*", + "vite.config.*", + "../../config/**", + "tsconfig.tsbuildinfo" + ], + "outputs": [ + // V1-Addon convention + "addon-test-support/**", + // V2-Addon convention + "dist/**", + "unstable-preview-types/**" + ], + "dependsOn": [], + "cache": false, + // https://turbo.build/repo/docs/reference/configuration#outputmode + "outputMode": "new-only" + }, + + // virtual task + "crawl-graph": { + "dependsOn": ["^crawl-graph"], + "cache": false + }, + + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + // + // Local Dev + // + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + "start": { + // "dependsOn": ["_task:sync-hardlinks", "^_build"], + // "outputs": [], + // "cache": false, + // "persistent": true + }, + + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + // + // Checks + // + ///////////////////////////////////////////////// + ///////////////////////////////////////////////// + + "lint": { + "inputs": ["eslint.*", "tsconfig.json", "tsconfig.tsbuildinfo"], + "dependsOn": ["^build:pkg"], + // https://turbo.build/repo/docs/reference/configuration#outputmode + "outputMode": "new-only" + }, + + "check:types": { + "inputs": ["tsconfig.json", "tsconfig.tsbuildinfo"], + "dependsOn": ["^build:pkg"], + // https://turbo.build/repo/docs/reference/configuration#outputmode + "outputMode": "new-only" + }, + + "build:tests": { + "inputs": [ + // + V2-Addon convention + "src/**", + "tests/**", + "addon-main.*", + "tsconfig.json", + "babel.*", + "vite.config.*" + ], + "outputs": [ + // V1-Addon convention + "addon/**", + "addon-test-support/**", + // V2-Addon convention + "dist/**", + "dist-test/**", + "declarations/**", + "unstable-preview-types/**" + ], + "dependsOn": ["^build:pkg"] + }, + "build:production": { + "inputs": [ + // + V2-Addon convention + "src/**", + "tests/**", + "ember-cli-build.js", + "addon-main.*", + "babel.*", + "vite.config.*" + ], + "outputs": [ + // V1-Addon convention + "addon/**", + "addon-test-support/**", + // V2-Addon convention + "dist/**", + "dist-test/**", + "declarations/**" + ], + "dependsOn": ["^build:pkg"] + }, + + "test": { + "inputs": ["../../packages/diagnostic/server/**"], + "outputs": [], + "dependsOn": ["build:tests"] + }, + "test:production": { + "inputs": ["../../packages/diagnostic/server/**"], + "outputs": [], + "dependsOn": ["build:production"] + } + } +}