From ebe2c1acb4947f69c7ec8f671e74c5da6971dea1 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 27 Jul 2022 05:09:39 -0600 Subject: [PATCH] chore: InternalModel burndown (#8078) * slowly burning away * record lifecycle cleanup * fix teardown record assertion * fix test * extracts belongsTo into the model package, only 3 failing tests, one of which is probably a delete case * move hasMany into model, no attempt at tests passing yet * almost there * structurally, its all there. Time to fix a lot of tests * only 240ish internal model usages left in the lib * more work * the great restructuring, part 1 * port @types out of typescript PR * the great migration comes to a pause * fix tests * updates to docs * fix encapsulation test --- .eslintrc.js | 106 +- .../@ember/array/-private/enumerable.d.ts | 0 .../array/-private/mutable-enumerable.d.ts | 0 .../@ember/array/-private/native-array.d.ts | 0 .../types => @types}/@ember/array/index.d.ts | 14 +- .../@ember/array/mutable.d.ts | 0 .../types => @types}/@ember/array/proxy.d.ts | 0 .../types => @types}/@ember/debug/index.d.ts | 0 @types/@ember/object/compat.d.ts | 2 + .../@ember/object/promise-proxy-mixin.d.ts | 2 +- .../types => @types}/@ember/object/proxy.d.ts | 2 +- .../@ember/runloop/-private/backburner.d.ts | 21 +- .../@ember/runloop/index.d.ts | 0 @types/@ember/utils/index.d.ts | 35 + @types/@ember/version.d.ts | 1 + @types/@glimmer/tracking.d.ts | 3 + .../ember-data-qunit-asserts/index.d.ts | 4 +- @types/ember/index.d.ts | 9 + @types/fastboot/index.d.ts | 13 + @types/require/index.d.ts | 3 + .../q}/ds-model.ts | 6 +- .../q}/ember-data-json-api.ts | 0 .../q}/fetch-manager.ts | 10 +- .../q}/identifier.ts | 7 +- .../q}/minimum-adapter-interface.ts | 11 +- .../q}/minimum-serializer-interface.ts | 5 +- .../q}/promise-proxies.ts | 0 .../q}/record-data-json-api.ts | 0 .../q}/record-data-record-wrapper.ts | 0 .../q}/record-data-schemas.ts | 0 .../q}/record-data-store-wrapper.ts | 0 .../q}/record-data.ts | 0 .../q}/record-instance.ts | 2 +- .../q}/relationship-record-data.ts | 11 +- .../q}/schema-definition-service.ts | 0 .../q}/store.ts | 0 .../q}/utils.ts | 0 .../node-tests/fixtures/expected.js | 35 +- .../relationships/belongs-to-test.js | 7 +- .../acceptance/relationships/has-many-test.js | 9 +- .../-ember-data/tests/helpers/accessors.ts | 14 +- .../identifiers/configuration-test.ts | 4 +- .../integration/identifiers/scenarios-test.ts | 26 +- .../tests/integration/injection-test.js | 34 - .../record-data/record-data-errors-test.ts | 6 +- .../record-data/record-data-state-test.ts | 4 +- .../record-data/store-wrapper-test.ts | 2 +- .../tests/integration/records/load-test.js | 2 +- .../integration/records/rematerialize-test.js | 24 +- .../tests/integration/records/unload-test.js | 222 +--- .../integration/references/belongs-to-test.js | 25 - .../integration/references/has-many-test.js | 28 - .../inverse-relationships-test.js | 2 +- .../integration/request-state-service-test.ts | 19 +- .../tests/integration/snapshot-test.js | 12 +- .../custom-class-model-test.ts | 27 +- packages/-ember-data/tests/unit/model-test.js | 2 +- .../unit/model/relationships/has-many-test.js | 4 +- .../adapter-populated-record-array-test.js | 4 +- .../unit/record-arrays/record-array-test.js | 12 +- .../tests/unit/store/adapter-interop-test.js | 75 +- .../tests/unit/store/asserts-test.js | 4 +- .../-ember-data/tests/unit/store/push-test.js | 49 - .../polymorphic-relationship-payloads-test.js | 2 +- packages/-ember-data/yuidoc.json | 3 +- .../adapter/addon/-private/build-url-mixin.ts | 6 +- .../-private/utils/determine-body-promise.ts | 2 +- .../adapter/addon/-private/utils/fetch.ts | 2 +- .../-private/utils/parse-response-headers.ts | 2 +- .../-private/utils/serialize-into-hash.ts | 6 +- packages/adapter/addon/index.ts | 11 +- packages/adapter/addon/json-api.ts | 6 +- packages/adapter/addon/rest.ts | 10 +- packages/adapter/types/require/index.d.ts | 1 - packages/debug/addon/index.js | 8 +- packages/model/addon/-private/belongs-to.js | 14 +- .../addon/-private/{system => }/diff-array.ts | 0 packages/model/addon/-private/has-many.js | 13 +- packages/model/addon/-private/index.ts | 11 +- .../addon/-private/legacy-data-fetch.js} | 385 +++--- .../model/addon/-private/legacy-data-utils.ts | 92 ++ .../-private/legacy-relationships-support.ts | 709 +++++++++++ .../addon/-private/{system => }/many-array.ts | 35 +- .../-private/{system => }/model-for-mixin.ts | 2 +- packages/model/addon/-private/model.js | 23 +- .../model/addon/-private/notify-changes.ts | 30 +- .../{system => }/promise-belongs-to.ts | 17 +- .../{system => }/promise-many-array.ts | 2 +- packages/model/addon/-private/record-state.ts | 12 +- .../addon/-private}/references/belongs-to.ts | 248 +++- .../addon/-private}/references/has-many.ts | 206 ++- .../relationships => }/relationship-meta.ts | 10 +- packages/model/index.js | 3 + packages/model/package.json | 1 + .../addon-build-config-for-data-package.js | 4 +- .../addon/-private/graph/-edge-definition.ts | 6 +- .../addon/-private/graph/-operations.ts | 4 +- .../addon/-private/graph/-utils.ts | 8 +- .../record-data/addon/-private/graph/index.ts | 10 +- .../operations/add-to-related-records.ts | 2 +- .../operations/remove-from-related-records.ts | 2 +- .../operations/replace-related-record.ts | 14 +- .../operations/replace-related-records.ts | 15 +- .../graph/operations/update-relationship.ts | 2 +- .../addon/-private/normalize-link.ts | 2 +- .../record-data/addon/-private/record-data.ts | 20 +- .../relationships/state/belongs-to.ts | 6 +- .../-private/relationships/state/has-many.ts | 4 +- .../-private/relationships/state/implicit.ts | 2 +- .../integration/graph/edge-removal/helpers.ts | 2 +- .../integration/graph/edge-removal/setup.ts | 25 +- .../tests/integration/graph/graph-test.ts | 12 +- .../integration/graph/operations-test.ts | 16 +- .../types/@ember/polyfills/index.d.ts | 5 - .../addon/-private/{system => }/backburner.js | 0 .../addon/-private/{system => }/coerce-id.ts | 0 .../-private/{system/store => }/common.js | 0 .../addon/-private/{system => }/core-store.ts | 834 ++---------- .../-private/{system => }/errors-utils.js | 0 .../-private/{system => }/fetch-manager.ts | 31 +- packages/store/addon/-private/finders.js | 107 ++ .../addon/-private/identifer-debug-consts.ts | 3 + .../cache.ts => identifier-cache.ts} | 40 +- .../identifiers/is-stable-identifier.ts | 18 - .../-private/identifiers/utils/uuid-v4.ts | 80 -- .../-private/{system => }/identity-map.ts | 3 +- packages/store/addon/-private/index.ts | 31 +- .../store/addon/-private/instance-cache.ts | 301 ++++- .../store => }/internal-model-factory.ts | 33 +- .../{system => }/internal-model-map.ts | 5 +- .../addon/-private/model/internal-model.ts | 602 +++++++++ .../record.ts => model/record-reference.ts} | 50 +- .../{system => }/model/shim-model-class.ts | 33 +- .../{system => }/normalize-model-name.ts | 0 .../-private/{system => }/promise-proxies.ts | 3 +- .../{system => }/promise-proxy-base.d.ts | 0 .../{system => }/promise-proxy-base.js | 0 .../{system => }/record-array-manager.ts | 34 +- .../adapter-populated-record-array.ts | 21 +- .../record-arrays/record-array.ts | 14 +- .../store/addon/-private/record-data-for.ts | 39 + .../store => }/record-data-store-wrapper.ts | 30 +- .../record-notification-manager.ts | 11 +- .../-private/{system => }/request-cache.ts | 11 +- .../{system => }/schema-definition-service.ts | 16 +- .../{system/store => }/serializer-response.ts | 13 +- .../{system => }/snapshot-record-array.ts | 6 +- .../addon/-private/{system => }/snapshot.ts | 50 +- .../-private/system/model/internal-model.ts | 1116 ----------------- .../addon/-private/system/record-arrays.ts | 8 - .../addon/-private/system/record-data-for.ts | 62 - .../store/addon/-private/system/references.js | 9 - .../-private/system/references/reference.ts | 205 --- .../-private/utils/construct-resource.ts | 10 +- .../addon/-private/utils/promise-record.ts | 23 +- .../addon/-private/{system => }/weak-cache.ts | 2 +- .../store/types/@ember/object/compat.d.ts | 2 - packages/store/types/@ember/utils/index.d.ts | 4 - packages/store/types/@ember/version.d.ts | 1 - packages/store/types/@glimmer/tracking.d.ts | 3 - packages/store/types/ember/index.d.ts | 5 - packages/store/types/fastboot/index.d.ts | 4 - packages/store/types/require/index.d.ts | 3 - .../qunit-asserts/assert-deprecation.ts | 2 +- root-tsconfig.json | 8 +- tsconfig.json | 91 +- 166 files changed, 3334 insertions(+), 3595 deletions(-) rename {packages/store/types => @types}/@ember/array/-private/enumerable.d.ts (100%) rename {packages/store/types => @types}/@ember/array/-private/mutable-enumerable.d.ts (100%) rename {packages/store/types => @types}/@ember/array/-private/native-array.d.ts (100%) rename {packages/store/types => @types}/@ember/array/index.d.ts (95%) rename {packages/store/types => @types}/@ember/array/mutable.d.ts (100%) rename {packages/store/types => @types}/@ember/array/proxy.d.ts (100%) rename {packages/store/types => @types}/@ember/debug/index.d.ts (100%) create mode 100644 @types/@ember/object/compat.d.ts rename {packages/store/types => @types}/@ember/object/promise-proxy-mixin.d.ts (93%) rename {packages/store/types => @types}/@ember/object/proxy.d.ts (94%) rename {packages/store/types => @types}/@ember/runloop/-private/backburner.d.ts (58%) rename {packages/store/types => @types}/@ember/runloop/index.d.ts (100%) create mode 100644 @types/@ember/utils/index.d.ts create mode 100644 @types/@ember/version.d.ts create mode 100644 @types/@glimmer/tracking.d.ts rename {packages/store/types => @types}/ember-data-qunit-asserts/index.d.ts (96%) create mode 100644 @types/ember/index.d.ts create mode 100644 @types/fastboot/index.d.ts create mode 100644 @types/require/index.d.ts rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/ds-model.ts (92%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/ember-data-json-api.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/fetch-manager.ts (76%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/identifier.ts (97%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/minimum-adapter-interface.ts (98%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/minimum-serializer-interface.ts (99%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/promise-proxies.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-data-json-api.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-data-record-wrapper.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-data-schemas.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-data-store-wrapper.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-data.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/record-instance.ts (93%) rename {packages/record-data/addon/-private/ts-interfaces => ember-data-types/q}/relationship-record-data.ts (60%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/schema-definition-service.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/store.ts (100%) rename {packages/store/addon/-private/ts-interfaces => ember-data-types/q}/utils.ts (100%) delete mode 100644 packages/-ember-data/tests/integration/injection-test.js delete mode 100644 packages/adapter/types/require/index.d.ts rename packages/model/addon/-private/{system => }/diff-array.ts (100%) rename packages/{store/addon/-private/system/store/finders.js => model/addon/-private/legacy-data-fetch.js} (74%) create mode 100644 packages/model/addon/-private/legacy-data-utils.ts create mode 100644 packages/model/addon/-private/legacy-relationships-support.ts rename packages/model/addon/-private/{system => }/many-array.ts (90%) rename packages/model/addon/-private/{system => }/model-for-mixin.ts (97%) rename packages/model/addon/-private/{system => }/promise-belongs-to.ts (81%) rename packages/model/addon/-private/{system => }/promise-many-array.ts (98%) rename packages/{store/addon/-private/system => model/addon/-private}/references/belongs-to.ts (60%) rename packages/{store/addon/-private/system => model/addon/-private}/references/has-many.ts (67%) rename packages/model/addon/-private/{system/relationships => }/relationship-meta.ts (88%) delete mode 100644 packages/record-data/types/@ember/polyfills/index.d.ts rename packages/store/addon/-private/{system => }/backburner.js (100%) rename packages/store/addon/-private/{system => }/coerce-id.ts (100%) rename packages/store/addon/-private/{system/store => }/common.js (100%) rename packages/store/addon/-private/{system => }/core-store.ts (74%) rename packages/store/addon/-private/{system => }/errors-utils.js (100%) rename packages/store/addon/-private/{system => }/fetch-manager.ts (95%) create mode 100644 packages/store/addon/-private/finders.js create mode 100644 packages/store/addon/-private/identifer-debug-consts.ts rename packages/store/addon/-private/{identifiers/cache.ts => identifier-cache.ts} (95%) delete mode 100644 packages/store/addon/-private/identifiers/is-stable-identifier.ts delete mode 100644 packages/store/addon/-private/identifiers/utils/uuid-v4.ts rename packages/store/addon/-private/{system => }/identity-map.ts (94%) rename packages/store/addon/-private/{system/store => }/internal-model-factory.ts (93%) rename packages/store/addon/-private/{system => }/internal-model-map.ts (95%) create mode 100644 packages/store/addon/-private/model/internal-model.ts rename packages/store/addon/-private/{system/references/record.ts => model/record-reference.ts} (83%) rename packages/store/addon/-private/{system => }/model/shim-model-class.ts (61%) rename packages/store/addon/-private/{system => }/normalize-model-name.ts (100%) rename packages/store/addon/-private/{system => }/promise-proxies.ts (98%) rename packages/store/addon/-private/{system => }/promise-proxy-base.d.ts (100%) rename packages/store/addon/-private/{system => }/promise-proxy-base.js (100%) rename packages/store/addon/-private/{system => }/record-array-manager.ts (92%) rename packages/store/addon/-private/{system => }/record-arrays/adapter-populated-record-array.ts (87%) rename packages/store/addon/-private/{system => }/record-arrays/record-array.ts (95%) create mode 100644 packages/store/addon/-private/record-data-for.ts rename packages/store/addon/-private/{system/store => }/record-data-store-wrapper.ts (90%) rename packages/store/addon/-private/{system => }/record-notification-manager.ts (90%) rename packages/store/addon/-private/{system => }/request-cache.ts (92%) rename packages/store/addon/-private/{system => }/schema-definition-service.ts (87%) rename packages/store/addon/-private/{system/store => }/serializer-response.ts (85%) rename packages/store/addon/-private/{system => }/snapshot-record-array.ts (96%) rename packages/store/addon/-private/{system => }/snapshot.ts (90%) delete mode 100644 packages/store/addon/-private/system/model/internal-model.ts delete mode 100644 packages/store/addon/-private/system/record-arrays.ts delete mode 100644 packages/store/addon/-private/system/record-data-for.ts delete mode 100644 packages/store/addon/-private/system/references.js delete mode 100644 packages/store/addon/-private/system/references/reference.ts rename packages/store/addon/-private/{system => }/weak-cache.ts (98%) delete mode 100644 packages/store/types/@ember/object/compat.d.ts delete mode 100644 packages/store/types/@ember/utils/index.d.ts delete mode 100644 packages/store/types/@ember/version.d.ts delete mode 100644 packages/store/types/@glimmer/tracking.d.ts delete mode 100644 packages/store/types/ember/index.d.ts delete mode 100644 packages/store/types/fastboot/index.d.ts delete mode 100644 packages/store/types/require/index.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index fb3f3663bfc..4d7f8046bc6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -160,16 +160,16 @@ module.exports = { 'packages/unpublished-fastboot-test-app/app/config/environment.d.ts', 'packages/unpublished-fastboot-test-app/app/app.ts', 'packages/unpublished-fastboot-test-app/app/adapters/application.ts', - 'packages/store/types/require/index.d.ts', - 'packages/store/types/qunit/index.d.ts', - 'packages/store/types/fastboot/index.d.ts', - 'packages/store/types/ember/index.d.ts', - 'packages/store/types/@glimmer/tracking.d.ts', - 'packages/store/types/@ember/utils/index.d.ts', - 'packages/store/types/@ember/runloop/index.d.ts', - 'packages/store/types/@ember/runloop/-private/backburner.d.ts', - 'packages/store/types/@ember/object/compat.d.ts', - 'packages/store/types/@ember/debug/index.d.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/tests/dummy/app/routes/application/route.ts', 'packages/store/tests/dummy/app/router.ts', 'packages/store/tests/dummy/app/resolver.ts', @@ -179,54 +179,49 @@ module.exports = { 'packages/store/addon/-private/utils/promise-record.ts', 'packages/store/addon/-private/utils/is-non-empty-string.ts', 'packages/store/addon/-private/utils/construct-resource.ts', - 'packages/store/addon/-private/ts-interfaces/utils.ts', - 'packages/store/addon/-private/ts-interfaces/schema-definition-service.ts', - 'packages/store/addon/-private/ts-interfaces/record-instance.ts', - 'packages/store/addon/-private/ts-interfaces/record-data.ts', - 'packages/store/addon/-private/ts-interfaces/record-data-store-wrapper.ts', - 'packages/store/addon/-private/ts-interfaces/record-data-schemas.ts', - 'packages/store/addon/-private/ts-interfaces/record-data-record-wrapper.ts', - 'packages/store/addon/-private/ts-interfaces/record-data-json-api.ts', - 'packages/store/addon/-private/ts-interfaces/promise-proxies.ts', - 'packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts', - 'packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts', - 'packages/store/addon/-private/ts-interfaces/identifier.ts', - 'packages/store/addon/-private/ts-interfaces/fetch-manager.ts', - 'packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts', - 'packages/store/addon/-private/ts-interfaces/ds-model.ts', - 'packages/store/addon/-private/system/store/record-data-store-wrapper.ts', - 'packages/store/addon/-private/system/store/internal-model-factory.ts', - 'packages/store/addon/-private/system/snapshot.ts', - 'packages/store/addon/-private/system/snapshot-record-array.ts', - 'packages/store/addon/-private/system/schema-definition-service.ts', - 'packages/store/addon/-private/system/request-cache.ts', - 'packages/store/addon/-private/system/references/belongs-to.ts', - 'packages/store/addon/-private/system/references/has-many.ts', - 'packages/store/addon/-private/system/references/reference.ts', - 'packages/store/addon/-private/system/references/record.ts', - 'packages/store/addon/-private/system/record-notification-manager.ts', - 'packages/store/addon/-private/system/record-data-for.ts', - 'packages/store/addon/-private/system/record-arrays.ts', - 'packages/store/addon/-private/system/normalize-model-name.ts', - 'packages/store/addon/-private/system/model/shim-model-class.ts', - 'packages/store/addon/-private/system/model/internal-model.ts', - 'packages/store/addon/-private/system/internal-model-map.ts', - 'packages/store/addon/-private/system/identity-map.ts', - 'packages/store/addon/-private/system/fetch-manager.ts', - 'packages/store/addon/-private/system/core-store.ts', - 'packages/store/addon/-private/system/coerce-id.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.ts', + 'ember-data-types/q/record-data-store-wrapper.ts', + 'ember-data-types/q/record-data-schemas.ts', + 'ember-data-types/q/record-data-record-wrapper.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/addon/-private/record-data-store-wrapper.ts', + 'packages/store/addon/-private/internal-model-factory.ts', + 'packages/store/addon/-private/snapshot.ts', + 'packages/store/addon/-private/snapshot-record-array.ts', + 'packages/store/addon/-private/schema-definition-service.ts', + 'packages/store/addon/-private/request-cache.ts', + 'packages/store/addon/-private/references/reference.ts', + 'packages/store/addon/-private/references/record.ts', + 'packages/store/addon/-private/record-notification-manager.ts', + 'packages/store/addon/-private/record-data-for.ts', + 'packages/store/addon/-private/normalize-model-name.ts', + 'packages/store/addon/-private/model/shim-model-class.ts', + 'packages/store/addon/-private/model/internal-model.ts', + 'packages/store/addon/-private/internal-model-map.ts', + 'packages/store/addon/-private/identity-map.ts', + 'packages/store/addon/-private/fetch-manager.ts', + 'packages/store/addon/-private/core-store.ts', + 'packages/store/addon/-private/coerce-id.ts', 'packages/store/addon/-private/index.ts', - 'packages/store/addon/-private/identifiers/utils/uuid-v4.ts', - 'packages/store/addon/-private/identifiers/is-stable-identifier.ts', - 'packages/store/addon/-private/identifiers/cache.ts', + 'packages/store/addon/-private/identifier-cache.ts', 'packages/serializer/tests/dummy/app/routes/application/route.ts', 'packages/serializer/tests/dummy/app/router.ts', 'packages/serializer/tests/dummy/app/resolver.ts', 'packages/serializer/tests/dummy/app/config/environment.d.ts', 'packages/serializer/tests/dummy/app/app.ts', 'packages/serializer/addon/index.ts', - 'packages/record-data/types/@ember/runloop/index.d.ts', - 'packages/record-data/types/@ember/polyfills/index.d.ts', + '@types/@ember/runloop/index.d.ts', + '@types/@ember/polyfills/index.d.ts', 'packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts', 'packages/record-data/tests/integration/graph/graph-test.ts', 'packages/record-data/tests/integration/graph/edge-test.ts', @@ -239,7 +234,7 @@ module.exports = { 'packages/record-data/tests/dummy/app/resolver.ts', 'packages/record-data/tests/dummy/app/config/environment.d.ts', 'packages/record-data/tests/dummy/app/app.ts', - 'packages/record-data/addon/-private/ts-interfaces/relationship-record-data.ts', + 'ember-data-types/q/relationship-record-data.ts', 'packages/record-data/addon/-private/relationships/state/implicit.ts', 'packages/record-data/addon/-private/relationships/state/has-many.ts', 'packages/record-data/addon/-private/relationships/state/belongs-to.ts', @@ -268,9 +263,10 @@ module.exports = { 'packages/model/tests/dummy/app/app.ts', 'packages/model/addon/index.ts', 'packages/model/addon/-private/util.ts', - 'packages/model/addon/-private/system/relationships/relationship-meta.ts', - 'packages/model/addon/-private/system/promise-many-array.ts', - 'packages/model/addon/-private/system/model-for-mixin.ts', + 'packages/model/addon/-private/relationship-meta.ts', + 'packages/model/addon/-private/legacy-relationships-support.ts', + 'packages/model/addon/-private/promise-many-array.ts', + 'packages/model/addon/-private/model-for-mixin.ts', 'packages/model/addon/-private/record-state.ts', 'packages/model/addon/-private/notify-changes.ts', 'packages/model/addon/-private/index.ts', diff --git a/packages/store/types/@ember/array/-private/enumerable.d.ts b/@types/@ember/array/-private/enumerable.d.ts similarity index 100% rename from packages/store/types/@ember/array/-private/enumerable.d.ts rename to @types/@ember/array/-private/enumerable.d.ts diff --git a/packages/store/types/@ember/array/-private/mutable-enumerable.d.ts b/@types/@ember/array/-private/mutable-enumerable.d.ts similarity index 100% rename from packages/store/types/@ember/array/-private/mutable-enumerable.d.ts rename to @types/@ember/array/-private/mutable-enumerable.d.ts diff --git a/packages/store/types/@ember/array/-private/native-array.d.ts b/@types/@ember/array/-private/native-array.d.ts similarity index 100% rename from packages/store/types/@ember/array/-private/native-array.d.ts rename to @types/@ember/array/-private/native-array.d.ts diff --git a/packages/store/types/@ember/array/index.d.ts b/@types/@ember/array/index.d.ts similarity index 95% rename from packages/store/types/@ember/array/index.d.ts rename to @types/@ember/array/index.d.ts index b9314843e77..a42f0e51ffd 100644 --- a/packages/store/types/@ember/array/index.d.ts +++ b/@types/@ember/array/index.d.ts @@ -12,12 +12,6 @@ import type NativeArray from '@ember/array/-private/native-array'; import type ComputedProperty from '@ember/object/computed'; import type Mixin from '@ember/object/mixin'; -namespace Array { - // detect is an intimate Mixin API, likely should not be typed upstream - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function detect(arr: unknown): boolean; -} - /** * 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. @@ -97,11 +91,15 @@ interface Array extends Enumerable { } // Ember.Array rather than Array because the `array-type` lint rule doesn't realize the global is shadowed // tslint:disable-next-line:array-type -declare const Array: Mixin>; -export default Array; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +declare class Array extends Mixin> { + static detect(arr: unknown): boolean; +} export const NativeArray; +export default Array; + /** * Creates an `Ember.NativeArray` from an Array like object. * Does not modify the original object's contents. Ember.A is not needed if diff --git a/packages/store/types/@ember/array/mutable.d.ts b/@types/@ember/array/mutable.d.ts similarity index 100% rename from packages/store/types/@ember/array/mutable.d.ts rename to @types/@ember/array/mutable.d.ts diff --git a/packages/store/types/@ember/array/proxy.d.ts b/@types/@ember/array/proxy.d.ts similarity index 100% rename from packages/store/types/@ember/array/proxy.d.ts rename to @types/@ember/array/proxy.d.ts diff --git a/packages/store/types/@ember/debug/index.d.ts b/@types/@ember/debug/index.d.ts similarity index 100% rename from packages/store/types/@ember/debug/index.d.ts rename to @types/@ember/debug/index.d.ts diff --git a/@types/@ember/object/compat.d.ts b/@types/@ember/object/compat.d.ts new file mode 100644 index 00000000000..1f5d4b3c2c8 --- /dev/null +++ b/@types/@ember/object/compat.d.ts @@ -0,0 +1,2 @@ +export function dependentKeyCompat(desc: PropertyDescriptor): void; +export function dependentKeyCompat(target: object, key: string, desc: PropertyDescriptor): void; diff --git a/packages/store/types/@ember/object/promise-proxy-mixin.d.ts b/@types/@ember/object/promise-proxy-mixin.d.ts similarity index 93% rename from packages/store/types/@ember/object/promise-proxy-mixin.d.ts rename to @types/@ember/object/promise-proxy-mixin.d.ts index ebf65239796..6939b4057a8 100755 --- a/packages/store/types/@ember/object/promise-proxy-mixin.d.ts +++ b/@types/@ember/object/promise-proxy-mixin.d.ts @@ -28,5 +28,5 @@ interface PromiseProxyMixin extends Promise { */ promise: Promise; } -declare class PromiseProxyMixin extends Promise {} +declare class PromiseProxyMixin extends Promise {} export default PromiseProxyMixin; diff --git a/packages/store/types/@ember/object/proxy.d.ts b/@types/@ember/object/proxy.d.ts similarity index 94% rename from packages/store/types/@ember/object/proxy.d.ts rename to @types/@ember/object/proxy.d.ts index 47b8a8ea402..b687c1c3db0 100755 --- a/packages/store/types/@ember/object/proxy.d.ts +++ b/@types/@ember/object/proxy.d.ts @@ -9,7 +9,7 @@ import { * `Ember.ObjectProxy` forwards all properties not defined by the proxy itself * to a proxied `content` object. */ -export default class ObjectProxy extends EmberObject { +export default class ObjectProxy extends EmberObject { /** * The object whose properties will be forwarded. */ diff --git a/packages/store/types/@ember/runloop/-private/backburner.d.ts b/@types/@ember/runloop/-private/backburner.d.ts similarity index 58% rename from packages/store/types/@ember/runloop/-private/backburner.d.ts rename to @types/@ember/runloop/-private/backburner.d.ts index 7b3f3331284..01c0c352833 100644 --- a/packages/store/types/@ember/runloop/-private/backburner.d.ts +++ b/@types/@ember/runloop/-private/backburner.d.ts @@ -1,3 +1,5 @@ +import type { EmberRunTimer } from '@ember/runloop/types'; + export interface QueueItem { method: string; target: object; @@ -6,10 +8,17 @@ export interface QueueItem { } export interface DeferredActionQueues { - [index: string]: any; + [index: string]: unknown; queues: object; - schedule(queueName: string, target: any, method: any, args: any, onceFlag: boolean, stack: any): any; - flush(fromAutorun: boolean): any; + schedule( + queueName: string, + target: unknown, + method: unknown, + args: unknown, + onceFlag: boolean, + stack: unknown + ): unknown; + flush(fromAutorun: boolean): unknown; } export interface DebugInfo { @@ -21,10 +30,10 @@ export interface DebugInfo { export interface Backburner { join(fn: () => T): T; - on(...args: any[]): void; - scheduleOnce(...args: any[]): void; + on(...args: unknown[]): void; + scheduleOnce(...args: unknown[]): EmberRunTimer; run(fn: () => T): T; - schedule(queueName: string, target: object | null, method: (() => void) | string): void; + schedule(queueName: string, target: object | null, method: (() => void) | string): EmberRunTimer; ensureInstance(): void; DEBUG: boolean; getDebugInfo(): DebugInfo; diff --git a/packages/store/types/@ember/runloop/index.d.ts b/@types/@ember/runloop/index.d.ts similarity index 100% rename from packages/store/types/@ember/runloop/index.d.ts rename to @types/@ember/runloop/index.d.ts diff --git a/@types/@ember/utils/index.d.ts b/@types/@ember/utils/index.d.ts new file mode 100644 index 00000000000..61934b4ff0d --- /dev/null +++ b/@types/@ember/utils/index.d.ts @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 00000000000..5ca42bd40c3 --- /dev/null +++ b/@types/@ember/version.d.ts @@ -0,0 +1 @@ +export const VERSION: string; diff --git a/@types/@glimmer/tracking.d.ts b/@types/@glimmer/tracking.d.ts new file mode 100644 index 00000000000..b15344d3d05 --- /dev/null +++ b/@types/@glimmer/tracking.d.ts @@ -0,0 +1,3 @@ +export function cached(target: object, key: string, desc: PropertyDescriptor): void; + +export function tracked(target: object, key: string): void; diff --git a/packages/store/types/ember-data-qunit-asserts/index.d.ts b/@types/ember-data-qunit-asserts/index.d.ts similarity index 96% rename from packages/store/types/ember-data-qunit-asserts/index.d.ts rename to @types/ember-data-qunit-asserts/index.d.ts index c009d5165ec..7d6ca4417c7 100644 --- a/packages/store/types/ember-data-qunit-asserts/index.d.ts +++ b/@types/ember-data-qunit-asserts/index.d.ts @@ -23,7 +23,7 @@ declare global { expectNoAssertion(callback: () => unknown): Promise; } - declare namespace QUnit { + namespace QUnit { export interface Assert { expectDeprecation(callback: () => unknown, options: DeprecationConfig | string | RegExp): Promise; expectNoDeprecation(callback: () => unknown): Promise; @@ -34,7 +34,7 @@ declare global { } } - declare interface QUnit { + interface QUnit { assert: Assert; } } diff --git a/@types/ember/index.d.ts b/@types/ember/index.d.ts new file mode 100644 index 00000000000..19ad6401313 --- /dev/null +++ b/@types/ember/index.d.ts @@ -0,0 +1,9 @@ +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/fastboot/index.d.ts b/@types/fastboot/index.d.ts new file mode 100644 index 00000000000..3835d61286a --- /dev/null +++ b/@types/fastboot/index.d.ts @@ -0,0 +1,13 @@ +interface Request { + protocol: string; + host: string; +} +export interface FastBoot { + require(moduleName: string): unknown; + isFastBoot: boolean; + request: Request; +} + +declare global { + const FastBoot: undefined | FastBoot; +} diff --git a/@types/require/index.d.ts b/@types/require/index.d.ts new file mode 100644 index 00000000000..67fffb84de9 --- /dev/null +++ b/@types/require/index.d.ts @@ -0,0 +1,3 @@ +export default function (moduleName: string): unknown; + +export function has(moduleName: string): boolean; diff --git a/packages/store/addon/-private/ts-interfaces/ds-model.ts b/ember-data-types/q/ds-model.ts similarity index 92% rename from packages/store/addon/-private/ts-interfaces/ds-model.ts rename to ember-data-types/q/ds-model.ts index 89739b291af..1915075a33f 100644 --- a/packages/store/addon/-private/ts-interfaces/ds-model.ts +++ b/ember-data-types/q/ds-model.ts @@ -1,16 +1,16 @@ import type EmberObject from '@ember/object'; import type { Errors } from '@ember-data/model/-private'; +import type Store from '@ember-data/store'; +import type InternalModel from '@ember-data/store/-private/model/internal-model'; -import type CoreStore from '../system/core-store'; -import type InternalModel from '../system/model/internal-model'; import type { JsonApiValidationError } 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: CoreStore; + store: Store; errors: Errors; _internalModel: InternalModel; toString(): string; diff --git a/packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts b/ember-data-types/q/ember-data-json-api.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts rename to ember-data-types/q/ember-data-json-api.ts diff --git a/packages/store/addon/-private/ts-interfaces/fetch-manager.ts b/ember-data-types/q/fetch-manager.ts similarity index 76% rename from packages/store/addon/-private/ts-interfaces/fetch-manager.ts rename to ember-data-types/q/fetch-manager.ts index a16f5049f6a..baf1818ab63 100644 --- a/packages/store/addon/-private/ts-interfaces/fetch-manager.ts +++ b/ember-data-types/q/fetch-manager.ts @@ -1,4 +1,4 @@ -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { Dict } from '@ember-data/types/q/utils'; import type { RecordIdentifier } from './identifier'; @@ -25,14 +25,10 @@ export interface Request { options?: any; } -export enum RequestStateEnum { - pending = 'pending', - fulfilled = 'fulfilled', - rejected = 'rejected', -} +export type RequestStates = 'pending' | 'fulfilled' | 'rejected'; export interface RequestState { - state: RequestStateEnum; + state: RequestStates; type: 'query' | 'mutation'; request: Request; response?: Response; diff --git a/packages/store/addon/-private/ts-interfaces/identifier.ts b/ember-data-types/q/identifier.ts similarity index 97% rename from packages/store/addon/-private/ts-interfaces/identifier.ts rename to ember-data-types/q/identifier.ts index ee3f9f79287..ca6b65efdfe 100644 --- a/packages/store/addon/-private/ts-interfaces/identifier.ts +++ b/ember-data-types/q/identifier.ts @@ -1,15 +1,14 @@ /** @module @ember-data/store */ + +import { DEBUG_CLIENT_ORIGINATED, DEBUG_IDENTIFIER_BUCKET } from '@ember-data/store/-private/identifer-debug-consts'; + import type { ExistingResourceObject, ResourceIdentifierObject } from './ember-data-json-api'; export type ResourceData = ResourceIdentifierObject | ExistingResourceObject; export type IdentifierBucket = 'record'; -// 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 interface Identifier { lid: string; clientId?: string; diff --git a/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts b/ember-data-types/q/minimum-adapter-interface.ts similarity index 98% rename from packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts rename to ember-data-types/q/minimum-adapter-interface.ts index 6b7ce3f8927..81f52bc66e2 100644 --- a/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts +++ b/ember-data-types/q/minimum-adapter-interface.ts @@ -1,8 +1,9 @@ -import type Store from '../system/core-store'; -import type AdapterPopulatedRecordArray from '../system/record-arrays/adapter-populated-record-array'; -import type Snapshot from '../system/snapshot'; -import type SnapshotRecordArray from '../system/snapshot-record-array'; -import type { ModelSchema } from '../ts-interfaces/ds-model'; +import type Store from '@ember-data/store'; +import type AdapterPopulatedRecordArray from '@ember-data/store/-private/record-arrays/adapter-populated-record-array'; +import type Snapshot from '@ember-data/store/-private/snapshot'; +import type SnapshotRecordArray from '@ember-data/store/-private/snapshot-record-array'; + +import type { ModelSchema } from './ds-model'; import type { RelationshipSchema } from './record-data-schemas'; import type { Dict } from './utils'; diff --git a/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts b/ember-data-types/q/minimum-serializer-interface.ts similarity index 99% rename from packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts rename to ember-data-types/q/minimum-serializer-interface.ts index 1d1006466cb..81ac89e2f14 100644 --- a/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts +++ b/ember-data-types/q/minimum-serializer-interface.ts @@ -1,7 +1,8 @@ import type { Object as JSONObject } from 'json-typescript'; -import type Store from '../system/core-store'; -import type Snapshot from '../system/snapshot'; +import type Store from '@ember-data/store'; +import type Snapshot from '@ember-data/store/-private/snapshot'; + import type { ModelSchema } from './ds-model'; import type { JsonApiDocument, SingleResourceDocument } from './ember-data-json-api'; import type { AdapterPayload } from './minimum-adapter-interface'; diff --git a/packages/store/addon/-private/ts-interfaces/promise-proxies.ts b/ember-data-types/q/promise-proxies.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/promise-proxies.ts rename to ember-data-types/q/promise-proxies.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-data-json-api.ts b/ember-data-types/q/record-data-json-api.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/record-data-json-api.ts rename to ember-data-types/q/record-data-json-api.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-data-record-wrapper.ts b/ember-data-types/q/record-data-record-wrapper.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/record-data-record-wrapper.ts rename to ember-data-types/q/record-data-record-wrapper.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-data-schemas.ts b/ember-data-types/q/record-data-schemas.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/record-data-schemas.ts rename to ember-data-types/q/record-data-schemas.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-data-store-wrapper.ts b/ember-data-types/q/record-data-store-wrapper.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/record-data-store-wrapper.ts rename to ember-data-types/q/record-data-store-wrapper.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-data.ts b/ember-data-types/q/record-data.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/record-data.ts rename to ember-data-types/q/record-data.ts diff --git a/packages/store/addon/-private/ts-interfaces/record-instance.ts b/ember-data-types/q/record-instance.ts similarity index 93% rename from packages/store/addon/-private/ts-interfaces/record-instance.ts rename to ember-data-types/q/record-instance.ts index 3acef75cbd3..467f6cd2306 100644 --- a/packages/store/addon/-private/ts-interfaces/record-instance.ts +++ b/ember-data-types/q/record-instance.ts @@ -7,7 +7,7 @@ import type { Dict } from './utils'; /* 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 `_modelFactoryFor.create()` to + 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. diff --git a/packages/record-data/addon/-private/ts-interfaces/relationship-record-data.ts b/ember-data-types/q/relationship-record-data.ts similarity index 60% rename from packages/record-data/addon/-private/ts-interfaces/relationship-record-data.ts rename to ember-data-types/q/relationship-record-data.ts index 51bced03ef6..e5188b2d155 100644 --- a/packages/record-data/addon/-private/ts-interfaces/relationship-record-data.ts +++ b/ember-data-types/q/relationship-record-data.ts @@ -1,12 +1,9 @@ +import type BelongsToRelationship from '@ember-data/record-data/-private/relationships/state/belongs-to'; import type { RecordDataStoreWrapper } from '@ember-data/store/-private'; -import type { - CollectionResourceRelationship, - SingleResourceRelationship, -} from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; -import type BelongsToRelationship from '../relationships/state/belongs-to'; +import type { CollectionResourceRelationship, SingleResourceRelationship } from './ember-data-json-api'; +import type { RecordIdentifier, StableRecordIdentifier } from './identifier'; +import type { RecordData } from './record-data'; export interface DefaultSingleResourceRelationship extends SingleResourceRelationship { _relationship: BelongsToRelationship; diff --git a/packages/store/addon/-private/ts-interfaces/schema-definition-service.ts b/ember-data-types/q/schema-definition-service.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/schema-definition-service.ts rename to ember-data-types/q/schema-definition-service.ts diff --git a/packages/store/addon/-private/ts-interfaces/store.ts b/ember-data-types/q/store.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/store.ts rename to ember-data-types/q/store.ts diff --git a/packages/store/addon/-private/ts-interfaces/utils.ts b/ember-data-types/q/utils.ts similarity index 100% rename from packages/store/addon/-private/ts-interfaces/utils.ts rename to ember-data-types/q/utils.ts diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index 65e980fc6ee..688d966e8b9 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -87,12 +87,10 @@ module.exports = { '(private) @ember-data/store SnapshotRecordArray#_snapshots', '(private) @ember-data/store SnapshotRecordArray#constructor', '(private) @ember-data/store Store#_backburner', - '(private) @ember-data/store Store#_load', '(private) @ember-data/store Store#_push', '(private) @ember-data/store Store#find', '(private) @ember-data/store Store#init', '(private) @ember-data/store Store#recordWasInvalid', - '(private) @ember-data/store Store#setRecordId', '(public) @ember-data/adapter Adapter#coalesceFindRequests', '(public) @ember-data/adapter Adapter#createRecord', '(public) @ember-data/adapter Adapter#deleteRecord', @@ -278,11 +276,16 @@ 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 BelongsToReference#id', - '(public) @ember-data/store BelongsToReference#load', - '(public) @ember-data/store BelongsToReference#push', - '(public) @ember-data/store BelongsToReference#reload', - '(public) @ember-data/store BelongsToReference#value', + '(public) @ember-data/model BelongsToReference#id', + '(public) @ember-data/model BelongsToReference#load', + '(public) @ember-data/model BelongsToReference#push', + '(public) @ember-data/model BelongsToReference#reload', + '(public) @ember-data/model BelongsToReference#value', + '(public) @ember-data/model BelongsToReference#remoteType', + '(public) @ember-data/model BelongsToReference#value', + '(public) @ember-data/model BelongsToReference#link', + '(public) @ember-data/model BelongsToReference#links', + '(public) @ember-data/model BelongsToReference#meta', '(public) @ember-data/model Errors#add', '(public) @ember-data/model Errors#clear', '(public) @ember-data/model Errors#errorsFor', @@ -291,12 +294,15 @@ module.exports = { '(public) @ember-data/model Errors#length', '(public) @ember-data/model Errors#messages', '(public) @ember-data/model Errors#remove', - '(public) @ember-data/store HasManyReference#ids', - '(public) @ember-data/store HasManyReference#load', - '(public) @ember-data/store HasManyReference#push', - '(public) @ember-data/store HasManyReference#reload', - '(public) @ember-data/store HasManyReference#remoteType', - '(public) @ember-data/store HasManyReference#value', + '(public) @ember-data/model HasManyReference#ids', + '(public) @ember-data/model HasManyReference#load', + '(public) @ember-data/model HasManyReference#push', + '(public) @ember-data/model HasManyReference#reload', + '(public) @ember-data/model HasManyReference#remoteType', + '(public) @ember-data/model HasManyReference#value', + '(public) @ember-data/model HasManyReference#link', + '(public) @ember-data/model HasManyReference#links', + '(public) @ember-data/model HasManyReference#meta', '(public) @ember-data/store IdentifierCache#createIdentifierForNewRecord', '(public) @ember-data/store IdentifierCache#forgetRecordIdentifier', '(public) @ember-data/store IdentifierCache#getOrCreateRecordIdentifier', @@ -320,9 +326,6 @@ module.exports = { '(public) @ember-data/store RecordReference#reload', '(public) @ember-data/store RecordReference#remoteType', '(public) @ember-data/store RecordReference#value', - '(public) @ember-data/store Reference#link', - '(public) @ember-data/store Reference#meta', - '(public) @ember-data/store Reference#remoteType', '(public) @ember-data/store Snapshot#adapterOptions', '(public) @ember-data/store Snapshot#attr', '(public) @ember-data/store Snapshot#attributes', diff --git a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js index e03272e2a6c..e4d0a7b37ea 100644 --- a/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/belongs-to-test.js @@ -10,6 +10,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { ServerError } from '@ember-data/adapter/error'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { LEGACY_SUPPORT } from '@ember-data/model/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; @@ -261,7 +262,7 @@ module('async belongs-to rendering tests', function (hooks) { }, }); - const storeWrapper = store._storeWrapper; + const storeWrapper = store._instanceCache._storeWrapper; const identifier = pete._internalModel.identifier; const implicitRelationships = implicitRelationshipsFor(storeWrapper, identifier); const implicitKeys = Object.keys(implicitRelationships); @@ -516,8 +517,8 @@ module('async belongs-to rendering tests', function (hooks) { const relationship = sedona.belongsTo('parent').belongsToRelationship; const { state, definition } = relationship; - let RelationshipPromiseCache = sedona._internalModel._relationshipPromisesCache; - let RelationshipProxyCache = sedona._internalModel._relationshipProxyCache; + let RelationshipPromiseCache = LEGACY_SUPPORT.get(sedona)._relationshipPromisesCache; + let RelationshipProxyCache = LEGACY_SUPPORT.get(sedona)._relationshipProxyCache; assert.true(definition.isAsync, 'The relationship is async'); assert.false(state.isEmpty, 'The relationship is not empty'); diff --git a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js index 47c1328c664..0f29c76699f 100644 --- a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js @@ -15,6 +15,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { ServerError } from '@ember-data/adapter/error'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { LEGACY_SUPPORT } from '@ember-data/model/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; @@ -314,8 +315,8 @@ module('async has-many rendering tests', function (hooks) { assert.deepEqual(names, ['Selena has a parent'], 'We rendered only the names for successful requests'); let relationshipState = parent.hasMany('children').hasManyRelationship; - let RelationshipPromiseCache = parent._internalModel._relationshipPromisesCache; - let RelationshipProxyCache = parent._internalModel._relationshipProxyCache; + let RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; + let 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'); @@ -430,8 +431,8 @@ module('async has-many rendering tests', function (hooks) { assert.deepEqual(names, [], 'We rendered no names'); let relationshipState = parent.hasMany('children').hasManyRelationship; - let RelationshipPromiseCache = parent._internalModel._relationshipPromisesCache; - let RelationshipProxyCache = parent._internalModel._relationshipProxyCache; + let RelationshipPromiseCache = LEGACY_SUPPORT.get(parent)._relationshipPromisesCache; + let RelationshipProxyCache = LEGACY_SUPPORT.get(parent)._relationshipProxyCache; assert.true(relationshipState.definition.isAsync, 'The relationship is async'); assert.true( diff --git a/packages/-ember-data/tests/helpers/accessors.ts b/packages/-ember-data/tests/helpers/accessors.ts index 4efbe0f4e4d..e326c07febb 100644 --- a/packages/-ember-data/tests/helpers/accessors.ts +++ b/packages/-ember-data/tests/helpers/accessors.ts @@ -4,28 +4,28 @@ import type { Relationship as ImplicitRelationship, } from '@ember-data/record-data/-private'; import { graphFor } from '@ember-data/record-data/-private'; +import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import type { RecordDataStoreWrapper } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { ConfidentDict as RelationshipDict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { ConfidentDict as RelationshipDict } from '@ember-data/types/q/utils'; export function getRelationshipStateForRecord( - record: { store: CoreStore }, + record: { store: Store }, propertyName: string ): BelongsToRelationship | ManyRelationship | ImplicitRelationship { const identifier = recordIdentifierFor(record); - return graphFor(record.store._storeWrapper).get(identifier, propertyName); + return graphFor(record.store).get(identifier, propertyName); } export function hasRelationshipForRecord( record: { - store: CoreStore; + store: Store; }, propertyName: string ): boolean { const identifier = recordIdentifierFor(record); - const relationships = graphFor(record.store._storeWrapper).identifiers.get(identifier); + const relationships = graphFor(record.store).identifiers.get(identifier); return relationships ? propertyName in relationships : false; } diff --git a/packages/-ember-data/tests/integration/identifiers/configuration-test.ts b/packages/-ember-data/tests/integration/identifiers/configuration-test.ts index ce4d137e3fc..aca1153f290 100644 --- a/packages/-ember-data/tests/integration/identifiers/configuration-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/configuration-test.ts @@ -18,13 +18,13 @@ import Store, { setIdentifierResetMethod, setIdentifierUpdateMethod, } from '@ember-data/store'; -import { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; +import type { DSModel } from '@ember-data/types/q/ds-model'; import type { IdentifierBucket, ResourceData, StableIdentifier, StableRecordIdentifier, -} from '@ember-data/store/-private/ts-interfaces/identifier'; +} from '@ember-data/types/q/identifier'; module('Integration | Identifiers - configuration', function (hooks) { setupTest(hooks); diff --git a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts index 63f5a8752a1..66e6c443adc 100644 --- a/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts +++ b/packages/-ember-data/tests/integration/identifiers/scenarios-test.ts @@ -14,14 +14,14 @@ import Store, { setIdentifierResetMethod, setIdentifierUpdateMethod, } from '@ember-data/store'; -import { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; +import type { DSModel } from '@ember-data/types/q/ds-model'; import type { IdentifierBucket, ResourceData, StableIdentifier, StableRecordIdentifier, -} from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { ConfidentDict } from '@ember-data/store/-private/ts-interfaces/utils'; +} from '@ember-data/types/q/identifier'; +import type { ConfidentDict } from '@ember-data/types/q/utils'; function isNonEmptyString(str: any): str is string { return typeof str === 'string' && str.length > 0; @@ -181,8 +181,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -204,8 +202,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -236,8 +232,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -413,8 +407,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -438,8 +430,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -466,8 +456,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -501,8 +489,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -555,8 +541,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -590,8 +574,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( @@ -624,8 +606,6 @@ 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 internalModels = store._internalModelsFor('user')._models; - assert.strictEqual(internalModels.length, 1, 'Once settled there is only a single internal-model'); const lidCache = store.identifierCache._cache.lids; const lids = Object.keys(lidCache); assert.strictEqual( diff --git a/packages/-ember-data/tests/integration/injection-test.js b/packages/-ember-data/tests/integration/injection-test.js deleted file mode 100644 index 1ade06f6d75..00000000000 --- a/packages/-ember-data/tests/integration/injection-test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { module, test } from 'qunit'; - -import { setupTest } from 'ember-qunit'; - -module('integration/injection factoryFor enabled', function (hooks) { - setupTest(hooks); - let store; - let Model; - - hooks.beforeEach(function () { - let { owner } = this; - Model = { - isModel: true, - }; - owner.register('model:super-villain', Model); - store = owner.lookup('service:store'); - }); - - test('modelFactoryFor', function (assert) { - let { owner } = this; - const trueFactory = owner.factoryFor('model:super-villain'); - const modelFactory = store._modelFactoryFor('super-villain'); - - assert.strictEqual(modelFactory, trueFactory, 'expected the factory itself to be returned'); - }); - - test('modelFor', function (assert) { - const modelClass = store.modelFor('super-villain'); - - assert.strictEqual(modelClass, Model, 'expected the factory itself to be returned'); - - assert.strictEqual(modelClass.modelName, 'super-villain', 'expected the factory itself to be returned'); - }); -}); diff --git a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts index 85e128f1a49..5216991a255 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts @@ -9,9 +9,9 @@ import { InvalidError } from '@ember-data/adapter/error'; import Model, { attr } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; -import type { NewRecordIdentifier, RecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; -import type { JsonApiValidationError } from '@ember-data/store/-private/ts-interfaces/record-data-json-api'; +import type { NewRecordIdentifier, RecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; class Person extends Model { // TODO fix the typing for naked attrs diff --git a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts index f19c73dc377..77cc7cf7d3c 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts @@ -9,8 +9,8 @@ import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; -import type { NewRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; +import type { NewRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; class Person extends Model { // TODO fix the typing for naked attrs diff --git a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts index 994e754170a..1c8409dcd80 100644 --- a/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts +++ b/packages/-ember-data/tests/integration/record-data/store-wrapper-test.ts @@ -298,7 +298,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho if (count === 1) { recordData = storeWrapper.recordDataFor('house'); } else if (count === 2) { - internalModel = store._internalModelForResource({ type: 'house', lid }); + internalModel = store._instanceCache._internalModelForResource({ type: 'house', lid }); } } diff --git a/packages/-ember-data/tests/integration/records/load-test.js b/packages/-ember-data/tests/integration/records/load-test.js index f630be4ad95..dc1410b93ed 100644 --- a/packages/-ember-data/tests/integration/records/load-test.js +++ b/packages/-ember-data/tests/integration/records/load-test.js @@ -143,7 +143,7 @@ module('integration/load - Loading Records', function (hooks) { }) ); - let internalModel = store._internalModelForResource({ type: 'person', id: '1' }); + let internalModel = store._instanceCache._internalModelForResource({ type: 'person', id: '1' }); // test that our initial state is correct assert.true(internalModel.isEmpty, 'We begin in the empty state'); diff --git a/packages/-ember-data/tests/integration/records/rematerialize-test.js b/packages/-ember-data/tests/integration/records/rematerialize-test.js index 9759b9b1373..4ed83dbf51b 100644 --- a/packages/-ember-data/tests/integration/records/rematerialize-test.js +++ b/packages/-ember-data/tests/integration/records/rematerialize-test.js @@ -86,16 +86,16 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( - store._internalModelsFor('person').has('@ember-data:lid-person-1'), - 'The person internalModel is loaded' + !!store.identifierCache.peekRecordIdentifier({ lid: '@lid:person-1' }), + 'The person identifier is loaded' ); run(() => person.unloadRecord()); assert.strictEqual(store.peekRecord('person', '1'), null, 'The person is unloaded'); assert.false( - store._internalModelsFor('person').has('@ember-data:lid-person-1'), - 'The person internalModel is freed' + !!store.identifierCache.peekRecordIdentifier({ lid: '@lid:person-1' }), + 'The person identifier is freed' ); run(() => { @@ -215,11 +215,11 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) // assert our initial cache state assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( - store._internalModelsFor('person').has('@ember-data:lid-person-1'), - 'The person internalModel is loaded' + !!store.identifierCache.peekRecordIdentifier({ lid: '@lid:person-1' }), + 'The person identifier is loaded' ); assert.notStrictEqual(store.peekRecord('boat', '1'), null, 'The boat is in the store'); - assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is loaded'); + assert.true(!!store.identifierCache.peekRecordIdentifier({ lid: '@lid:boat-1' }), 'The boat identifier is loaded'); let boats = await adam.get('boats'); assert.strictEqual(boats.get('length'), 2, 'Before unloading boats.length is correct'); @@ -229,7 +229,10 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) // assert our new cache state assert.strictEqual(store.peekRecord('boat', '1'), null, 'The boat is unloaded'); - assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is retained'); + assert.true( + !!store.identifierCache.peekRecordIdentifier({ lid: '@lid:boat-1' }), + 'The boat identifier is retained' + ); // cause a rematerialization, this should also cause us to fetch boat '1' again boats = run(() => adam.get('boats')); @@ -242,6 +245,9 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) assert.notStrictEqual(rematerializedBoaty, boaty, 'the boat is rematerialized, not recycled'); assert.notStrictEqual(store.peekRecord('boat', '1'), null, 'The boat is loaded'); - assert.true(store._internalModelsFor('boat').has('@ember-data:lid-boat-1'), 'The boat internalModel is retained'); + assert.true( + !!store.identifierCache.peekRecordIdentifier({ lid: '@lid:boat-1' }), + 'The boat identifier is retained' + ); }); }); diff --git a/packages/-ember-data/tests/integration/records/unload-test.js b/packages/-ember-data/tests/integration/records/unload-test.js index c309ece37bf..26a2e7bd225 100644 --- a/packages/-ember-data/tests/integration/records/unload-test.js +++ b/packages/-ember-data/tests/integration/records/unload-test.js @@ -11,7 +11,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 { recordDataFor } from '@ember-data/store/-private'; function idsFromArr(arr) { return arr.map((i) => i.id); @@ -182,18 +181,16 @@ module('integration/unload - Unloading Records', function (hooks) { }); assert.strictEqual(store.peekAll('person').get('length'), 1, 'one person record loaded'); - assert.strictEqual(store._internalModelsFor('person').length, 1, 'one person internalModel loaded'); run(function () { adam.unloadRecord(); }); assert.strictEqual(store.peekAll('person').get('length'), 0, 'no person records'); - assert.strictEqual(store._internalModelsFor('person').length, 0, 'no person internalModels'); }); test('can unload all records for a given type', function (assert) { - assert.expect(10); + assert.expect(6); let car; run(function () { @@ -237,9 +234,7 @@ module('integration/unload - Unloading Records', function (hooks) { }); assert.strictEqual(store.peekAll('person').get('length'), 2, 'two person records loaded'); - assert.strictEqual(store._internalModelsFor('person').length, 2, 'two person internalModels loaded'); assert.strictEqual(store.peekAll('car').get('length'), 1, 'one car record loaded'); - assert.strictEqual(store._internalModelsFor('car').length, 1, 'one car internalModel loaded'); run(function () { car.get('person'); @@ -248,8 +243,6 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store.peekAll('person').get('length'), 0); assert.strictEqual(store.peekAll('car').get('length'), 1); - assert.strictEqual(store._internalModelsFor('person').length, 0, 'zero person internalModels loaded'); - assert.strictEqual(store._internalModelsFor('car').length, 1, 'one car internalModel loaded'); run(function () { store.push({ @@ -268,23 +261,10 @@ module('integration/unload - Unloading Records', function (hooks) { assert.ok(!!car, 'We have a car'); assert.notOk(person, 'We dont have a person'); - - /* - @runspired believes these asserts were incorrect on master. - Basically, we intentionally treat unload on a sync belongsTo as client-side - delete bc "bad reason" of legacy support for the mis-use of unloadRecord. - Because of this, there should be no way to resurrect the relationship without - receiving new relationship info which does not occur in this test. - He checked how master manages to do this, and discovered bad things. TL;DR - because the `person` relationship is never materialized, it's state was - not cleared on unload, and thus the client-side delete never happened as intended. - */ - // assert.strictEqual(person.get('id'), '1', 'Inverse can load relationship after the record is unloaded'); - // assert.strictEqual(person.get('name'), 'Richard II', 'Inverse can load relationship after the record is unloaded'); }); test('can unload all records', function (assert) { - assert.expect(8); + assert.expect(4); run(function () { store.push({ @@ -327,9 +307,7 @@ module('integration/unload - Unloading Records', function (hooks) { }); assert.strictEqual(store.peekAll('person').get('length'), 2, 'two person records loaded'); - assert.strictEqual(store._internalModelsFor('person').length, 2, 'two person internalModels loaded'); assert.strictEqual(store.peekAll('car').get('length'), 1, 'one car record loaded'); - assert.strictEqual(store._internalModelsFor('car').length, 1, 'one car internalModel loaded'); run(function () { store.unloadAll(); @@ -337,12 +315,10 @@ module('integration/unload - Unloading Records', function (hooks) { assert.strictEqual(store.peekAll('person').get('length'), 0); assert.strictEqual(store.peekAll('car').get('length'), 0); - assert.strictEqual(store._internalModelsFor('person').length, 0, 'zero person internalModels loaded'); - assert.strictEqual(store._internalModelsFor('car').length, 0, 'zero car internalModels loaded'); }); test('removes findAllCache after unloading all records', function (assert) { - assert.expect(4); + assert.expect(2); run(function () { store.push({ @@ -368,7 +344,6 @@ module('integration/unload - Unloading Records', function (hooks) { }); assert.strictEqual(store.peekAll('person').get('length'), 2, 'two person records loaded'); - assert.strictEqual(store._internalModelsFor('person').length, 2, 'two person internalModels loaded'); run(function () { store.peekAll('person'); @@ -376,7 +351,6 @@ module('integration/unload - Unloading Records', function (hooks) { }); assert.strictEqual(store.peekAll('person').get('length'), 0, 'zero person records loaded'); - assert.strictEqual(store._internalModelsFor('person').length, 0, 'zero person internalModels loaded'); }); test('unloading all records also updates record array from peekAll()', function (assert) { @@ -428,7 +402,7 @@ module('integration/unload - Unloading Records', function (hooks) { } test('unloadAll(type) does not leave stranded internalModels in relationships (rediscover via store.push)', async function (assert) { - assert.expect(15); + assert.expect(13); let person = store.push({ data: { @@ -447,14 +421,9 @@ module('integration/unload - Unloading Records', function (hooks) { }); let boat = store.peekRecord('boat', '1'); - let initialBoatInternalModel = boat._internalModel; let relationshipState = person.hasMany('boats').hasManyRelationship; - let knownPeople = store._internalModelsFor('person'); - let knownBoats = store._internalModelsFor('boat'); // ensure we loaded the people and boats - assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); - assert.strictEqual(knownBoats.models.length, 1, 'one boat record is loaded'); assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); @@ -473,8 +442,6 @@ module('integration/unload - Unloading Records', function (hooks) { }); // ensure that our new state is correct - assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); - assert.strictEqual(knownBoats.models.length, 0, 'no boat records are loaded'); assert.strictEqual(relationshipState.canonicalState.length, 1, 'canonical member size should still be 1'); assert.strictEqual(relationshipState.members.size, 1, 'members size should still be 1'); assert.strictEqual(get(peopleBoats, 'length'), 0, 'Our person thinks they have no boats'); @@ -485,18 +452,16 @@ module('integration/unload - Unloading Records', function (hooks) { }) ); - let reloadedBoat = store.peekRecord('boat', '1'); - let reloadedBoatInternalModel = reloadedBoat._internalModel; + store.peekRecord('boat', '1'); - assert.strictEqual( - reloadedBoatInternalModel, - initialBoatInternalModel, - 'after an unloadAll, subsequent fetch results in the same InternalModel' - ); + // ensure that our new state is correct + assert.strictEqual(relationshipState.canonicalState.length, 1, 'canonical member size should still be 1'); + assert.strictEqual(relationshipState.members.size, 1, 'members size should still be 1'); + assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has their boats'); }); test('unloadAll(type) does not leave stranded internalModels in relationships (rediscover via relationship reload)', function (assert) { - assert.expect(17); + assert.expect(15); adapter.findRecord = (store, type, id) => { assert.strictEqual(type.modelName, 'boat', 'We refetch the boat'); @@ -525,14 +490,9 @@ module('integration/unload - Unloading Records', function (hooks) { ); let boat = store.peekRecord('boat', '1'); - let initialBoatInternalModel = boat._internalModel; let relationshipState = person.hasMany('boats').hasManyRelationship; - let knownPeople = store._internalModelsFor('person'); - let knownBoats = store._internalModelsFor('boat'); // ensure we loaded the people and boats - assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); - assert.strictEqual(knownBoats.models.length, 1, 'one boat record is loaded'); assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); @@ -551,25 +511,20 @@ module('integration/unload - Unloading Records', function (hooks) { }); // ensure that our new state is correct - assert.strictEqual(knownPeople.models.length, 1, 'one person record is loaded'); - assert.strictEqual(knownBoats.models.length, 0, 'no boat records are loaded'); assert.strictEqual(relationshipState.canonicalState.length, 1, 'canonical member size should still be 1'); assert.strictEqual(relationshipState.members.size, 1, 'members size should still be 1'); assert.strictEqual(get(peopleBoats, 'length'), 0, 'Our person thinks they have no boats'); run(() => person.get('boats')); - let reloadedBoat = store.peekRecord('boat', '1'); - let reloadedBoatInternalModel = reloadedBoat._internalModel; + store.peekRecord('boat', '1'); - assert.strictEqual( - reloadedBoatInternalModel, - initialBoatInternalModel, - 'after an unloadAll, subsequent fetch results in the same InternalModel' - ); + assert.strictEqual(relationshipState.canonicalState.length, 1, 'canonical member size should still be 1'); + assert.strictEqual(relationshipState.members.size, 1, 'members size should still be 1'); + assert.strictEqual(get(peopleBoats, 'length'), 1, 'Our person has their boats'); }); - test('(regression) unloadRecord followed by push in the same run-loop', function (assert) { + test('(regression) unloadRecord followed by push in the same run-loop', async function (assert) { let person = run(() => store.push({ data: { @@ -589,22 +544,9 @@ module('integration/unload - Unloading Records', function (hooks) { ); let boat = store.peekRecord('boat', '1'); - let initialBoatInternalModel = boat._internalModel; let relationshipState = person.hasMany('boats').hasManyRelationship; - let knownPeople = store._internalModelsFor('person'); - let knownBoats = store._internalModelsFor('boat'); // ensure we loaded the people and boats - assert.deepEqual( - knownPeople.models.map((m) => m.id), - ['1'], - 'one person record is loaded' - ); - assert.deepEqual( - knownBoats.models.map((m) => m.id), - ['1'], - 'one boat record is loaded' - ); assert.notStrictEqual(store.peekRecord('person', '1'), null); assert.notStrictEqual(store.peekRecord('boat', '1'), null); @@ -621,18 +563,6 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => boat.unloadRecord()); // ensure that our new state is correct - assert.deepEqual( - knownPeople.models.map((m) => m.id), - ['1'], - 'one person record is loaded' - ); - assert.deepEqual( - knownBoats.models.map((m) => m.id), - ['1'], - 'one boat record is known' - ); - assert.strictEqual(knownBoats.models[0], initialBoatInternalModel, 'We still have our boat'); - assert.true(initialBoatInternalModel.isEmpty, 'Model is in the empty state'); assert.deepEqual(idsFromArr(relationshipState.canonicalState), ['1'], 'canonical member size should still be 1'); assert.deepEqual(idsFromArr(relationshipState.currentState), ['1'], 'members size should still be 1'); assert.strictEqual(get(peopleBoats, 'length'), 0, 'Our person thinks they have no boats'); @@ -644,15 +574,10 @@ module('integration/unload - Unloading Records', function (hooks) { ); let reloadedBoat = store.peekRecord('boat', '1'); - let reloadedBoatInternalModel = reloadedBoat._internalModel; assert.deepEqual(idsFromArr(relationshipState.canonicalState), ['1'], 'canonical member size should be 1'); assert.deepEqual(idsFromArr(relationshipState.currentState), ['1'], 'members size should be 1'); - assert.strictEqual( - reloadedBoatInternalModel, - initialBoatInternalModel, - 'after an unloadRecord, subsequent fetch results in the same InternalModel' - ); + assert.strictEqual(get(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 @@ -665,23 +590,22 @@ module('integration/unload - Unloading Records', function (hooks) { }); }); - let yaBoat = store.peekRecord('boat', '1'); - let yaBoatInternalModel = yaBoat._internalModel; + boat = store.peekRecord('boat', '1'); + assert.notStrictEqual(boat, null, 'we have a boat'); assert.deepEqual(idsFromArr(relationshipState.canonicalState), ['1'], 'canonical member size should be 1'); assert.deepEqual(idsFromArr(relationshipState.currentState), ['1'], 'members size should be 1'); - assert.strictEqual( - yaBoatInternalModel, - initialBoatInternalModel, - 'after an unloadRecord, subsequent same-loop push results in the same InternalModel' - ); - }); - - test('unloading a disconnected subgraph clears the relevant internal models', function (assert) { - adapter.shouldBackgroundReloadRecord = () => false; + assert.strictEqual(get(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(() => { - store.push({ + person.unloadRecord(); + newPerson = store.push({ data: { type: 'person', id: '1', @@ -690,96 +614,16 @@ module('integration/unload - Unloading Records', function (hooks) { }, relationships: { boats: { - data: [ - { type: 'boat', id: '1' }, - { type: 'boat', id: '2' }, - ], - }, - }, - }, - }); - }); - - run(() => { - store.push({ - data: { - type: 'boat', - id: '1', - attributes: { - name: 'Boaty McBoatface', - }, - relationships: { - person: { - data: { type: 'person', id: '1' }, - }, - }, - }, - }); - }); - - run(() => { - store.push({ - data: { - type: 'boat', - id: '2', - attributes: { - name: 'The jackson', - }, - relationships: { - person: { - data: { type: 'person', id: '1' }, + data: [{ type: 'boat', id: '1' }], }, }, }, }); }); - assert.strictEqual(store._internalModelsFor('person').models.length, 1, 'one person record is loaded'); - assert.strictEqual(store._internalModelsFor('boat').models.length, 2, 'two boat records are loaded'); - assert.notStrictEqual(store.peekRecord('person', '1'), null); - assert.notStrictEqual(store.peekRecord('boat', '1'), null); - assert.notStrictEqual(store.peekRecord('boat', '2'), null); - - let checkOrphanCalls = 0; - let cleanupOrphanCalls = 0; - - function countOrphanCalls(record) { - let internalModel = record._internalModel; - let recordData = recordDataFor(record); - let origCheck = internalModel._checkForOrphanedInternalModels; - let origCleanup = recordData._cleanupOrphanedRecordDatas; - - internalModel._checkForOrphanedInternalModels = function () { - ++checkOrphanCalls; - return origCheck.apply(record._internalModel, arguments); - }; - - recordData._cleanupOrphanedRecordDatas = function () { - ++cleanupOrphanCalls; - return origCleanup.apply(recordData, arguments); - }; - } - countOrphanCalls(store.peekRecord('person', 1)); - countOrphanCalls(store.peekRecord('boat', 1)); - countOrphanCalls(store.peekRecord('boat', 2)); - - // make sure relationships are initialized - return store - .peekRecord('person', 1) - .get('boats') - .then(() => { - run(() => { - store.peekRecord('person', 1).unloadRecord(); - store.peekRecord('boat', 1).unloadRecord(); - store.peekRecord('boat', 2).unloadRecord(); - }); - - assert.strictEqual(store._internalModelsFor('person').models.length, 0); - assert.strictEqual(store._internalModelsFor('boat').models.length, 0); - - assert.strictEqual(checkOrphanCalls, 3, 'each internalModel checks for cleanup'); - assert.strictEqual(cleanupOrphanCalls, 3, 'each model data tries to cleanup'); - }); + const relatedPerson = await boat.person; + assert.notStrictEqual(relatedPerson, person, 'the original record is gone'); + assert.strictEqual(relatedPerson, newPerson, 'we have a new related record'); }); test('Unloading a record twice only schedules destroy once', function (assert) { @@ -1693,7 +1537,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => person.unloadRecord()); - assert.false(boats.isDestroyed, 'ManyArray is not destroyed when 1 side is unloaded'); + 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'); assert.strictEqual(boat3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); @@ -2198,7 +2042,7 @@ module('integration/unload - Unloading Records', function (hooks) { run(() => person.unloadRecord()); - assert.false(spoons.isDestroyed, 'ManyArray is not destroyed when 1 side is unloaded'); + assert.true(spoons.isDestroyed, 'ManyArray is destroyed when 1 side is unloaded'); assert.strictEqual(spoon2.belongsTo('person').id(), '1', 'unload async is not treated as delete'); assert.strictEqual(spoon3.belongsTo('person').id(), '1', 'unload async is not treated as delete'); diff --git a/packages/-ember-data/tests/integration/references/belongs-to-test.js b/packages/-ember-data/tests/integration/references/belongs-to-test.js index 82747ae1901..0a9c7d6e926 100644 --- a/packages/-ember-data/tests/integration/references/belongs-to-test.js +++ b/packages/-ember-data/tests/integration/references/belongs-to-test.js @@ -124,31 +124,6 @@ module('integration/references/belongs-to', function (hooks) { assert.strictEqual(familyReference.link(), '/families/1'); }); - test('BelongsToReference#parent is a reference to the parent where the relationship is defined', function (assert) { - let store = this.owner.lookup('service:store'); - - var person; - run(function () { - person = store.push({ - data: { - type: 'person', - id: 1, - relationships: { - family: { - data: { type: 'family', id: 1 }, - }, - }, - }, - }); - }); - - var personReference = store.getReference('person', 1); - var familyReference = person.belongsTo('family'); - - assert.ok(personReference, 'person reference is present'); - assert.deepEqual(familyReference.parent, personReference, 'parent reference on BelongsToReference'); - }); - test('BelongsToReference#meta() returns the most recent meta for the relationship', async function (assert) { let store = this.owner.lookup('service:store'); diff --git a/packages/-ember-data/tests/integration/references/has-many-test.js b/packages/-ember-data/tests/integration/references/has-many-test.js index 33eecee03e8..1bd6b81b040 100755 --- a/packages/-ember-data/tests/integration/references/has-many-test.js +++ b/packages/-ember-data/tests/integration/references/has-many-test.js @@ -135,34 +135,6 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(personsReference.link(), '/families/1/persons'); }); - test('HasManyReference#parent is a reference to the parent where the relationship is defined', function (assert) { - let 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 familyReference = store.getReference('family', 1); - var personsReference = family.hasMany('persons'); - - assert.ok(familyReference, 'person reference is present'); - assert.deepEqual(personsReference.parent, familyReference, 'parent reference on HasManyReferencee'); - }); - test('HasManyReference#meta() returns the most recent meta for the relationship', function (assert) { let store = this.owner.lookup('service:store'); diff --git a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js index ee518ad9859..dd9152a8a24 100644 --- a/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js +++ b/packages/-ember-data/tests/integration/relationships/inverse-relationships-test.js @@ -678,7 +678,7 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' const identifier = comment._internalModel.identifier; - assert.false(graphFor(store._storeWrapper).identifiers.has(identifier), 'relationships are cleared'); + assert.false(graphFor(store).identifiers.has(identifier), 'relationships are cleared'); assert.ok(recordData.isDestroyed, 'recordData is destroyed'); }); }); diff --git a/packages/-ember-data/tests/integration/request-state-service-test.ts b/packages/-ember-data/tests/integration/request-state-service-test.ts index 5f4c3deab9b..df6f60f7592 100644 --- a/packages/-ember-data/tests/integration/request-state-service-test.ts +++ b/packages/-ember-data/tests/integration/request-state-service-test.ts @@ -8,8 +8,7 @@ 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 { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; -import type { RequestStateEnum } from '@ember-data/store/-private/ts-interfaces/fetch-manager'; +import type { DSModel } from '@ember-data/types/q/ds-model'; class Person extends Model { // TODO fix the typing for naked attrs @@ -39,7 +38,7 @@ module('integration/request-state-service - Request State Service', function (ho data: { type: 'person', id: '1', - lid: '@ember-data:lid-person-1', + lid: '@lid:person-1', attributes: { name: 'Scumbag Dale', }, @@ -98,7 +97,7 @@ module('integration/request-state-service - Request State Service', function (ho let lastRequest = requestService.getLastRequestForRecord(identifier); let requestStateResult = { type: 'query' as const, - state: 'fulfilled' as RequestStateEnum, + state: 'fulfilled' as const, request: { data: [requestOp] }, response: { data: normalizedHash }, }; @@ -121,7 +120,7 @@ module('integration/request-state-service - Request State Service', function (ho let lastSavingRequest = requestService.getLastRequestForRecord(identifier); let savingRequestStateResult = { type: 'mutation' as const, - state: 'fulfilled' as RequestStateEnum, + state: 'fulfilled' as const, request: { data: [savingRequestOp] }, response: { data: undefined }, }; @@ -142,7 +141,7 @@ module('integration/request-state-service - Request State Service', function (ho data: { type: 'person', id: '1', - lid: '@ember-data:lid-person-1', + lid: '@lid:person-1', attributes: { name: 'Scumbag Dale', }, @@ -195,9 +194,9 @@ module('integration/request-state-service - Request State Service', function (ho assert.strictEqual(request.type, 'query', 'request is a query'); assert.deepEqual(request.request.data[0], requestOp, 'request op is correct'); } else if (count === 1) { - let requestStateResult = { + const requestStateResult = { type: 'query' as const, - state: 'fulfilled' as RequestStateEnum, + state: 'fulfilled' as const, request: { data: [requestOp] }, response: { data: normalizedHash }, }; @@ -207,9 +206,9 @@ module('integration/request-state-service - Request State Service', function (ho assert.strictEqual(request.type, 'mutation', 'request is a mutation'); assert.deepEqual(request.request.data[0], savingRequestOp, 'request op is correct'); } else if (count === 3) { - let savingRequestStateResult = { + const savingRequestStateResult = { type: 'mutation' as const, - state: 'fulfilled' as RequestStateEnum, + state: 'fulfilled' as const, request: { data: [savingRequestOp] }, response: { data: undefined }, }; diff --git a/packages/-ember-data/tests/integration/snapshot-test.js b/packages/-ember-data/tests/integration/snapshot-test.js index bd02c388d5d..de352ae531b 100644 --- a/packages/-ember-data/tests/integration/snapshot-test.js +++ b/packages/-ember-data/tests/integration/snapshot-test.js @@ -117,12 +117,12 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.expect(3); let postClassLoaded = false; - let modelFactoryFor = store._modelFactoryFor; - store._modelFactoryFor = (name) => { + let modelFor = store.modelFor; + store.modelFor = (name) => { if (name === 'post') { postClassLoaded = true; } - return modelFactoryFor.call(store, name); + return modelFor.call(store, name); }; await store._push({ @@ -134,7 +134,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }, }); - let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); + let postInternalModel = store._instanceCache._internalModelForResource({ type: 'post', id: '1' }); let snapshot = await store._instanceCache.createSnapshot(postInternalModel.identifier); assert.false(postClassLoaded, 'model class is not eagerly loaded'); @@ -175,7 +175,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); + let postInternalModel = store._instanceCache._internalModelForResource({ type: 'post', id: '1' }); let snapshot = store._instanceCache.createSnapshot(postInternalModel.identifier); let expected = { author: undefined, @@ -200,7 +200,7 @@ module('integration/snapshot - Snapshot', function (hooks) { }, }); - let postInternalModel = store._internalModelForResource({ type: 'post', id: '1' }); + let postInternalModel = store._instanceCache._internalModelForResource({ type: 'post', id: '1' }); let snapshot = store._instanceCache.createSnapshot(postInternalModel.identifier); let expected = { author: undefined, diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts index 76bea106696..7afcfb5b65d 100644 --- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts @@ -7,21 +7,17 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import JSONAPISerializer from '@ember-data/serializer/json-api'; -import Store, { recordIdentifierFor } from '@ember-data/store'; +import Store from '@ember-data/store'; import type { RecordDataStoreWrapper, Snapshot } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type NotificationManager from '@ember-data/store/-private/system/record-notification-manager'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordDataRecordWrapper } from '@ember-data/store/-private/ts-interfaces/record-data-record-wrapper'; -import type { - AttributesSchema, - RelationshipsSchema, -} from '@ember-data/store/-private/ts-interfaces/record-data-schemas'; -import { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; -import type { SchemaDefinitionService } from '@ember-data/store/-private/ts-interfaces/schema-definition-service'; +import type NotificationManager from '@ember-data/store/-private/record-notification-manager'; +import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordDataRecordWrapper } from '@ember-data/types/q/record-data-record-wrapper'; +import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service'; module('unit/model - Custom Class Model', function (hooks) { - let store: CoreStore; + let store: Store; class Person { constructor(public store: Store) { this.store = store; @@ -445,7 +441,9 @@ module('unit/model - Custom Class Model', function (hooks) { ); }); - test('relationshipReferenceFor belongsTo', async function (assert) { + /* + 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; @@ -517,7 +515,7 @@ module('unit/model - Custom Class Model', function (hooks) { assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); }); - test('relationshipReferenceFor hasMany', async function (assert) { + tes('relationshipReferenceFor hasMany', async function (assert) { assert.expect(3); this.owner.register('service:store', CustomStore); store = this.owner.lookup('service:store') as Store; @@ -595,4 +593,5 @@ module('unit/model - Custom Class Model', function (hooks) { assert.strictEqual(relationship.type, 'house', 'house relationship type found'); assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); }); + */ }); diff --git a/packages/-ember-data/tests/unit/model-test.js b/packages/-ember-data/tests/unit/model-test.js index 80cb5a1ff41..d25a8625f29 100644 --- a/packages/-ember-data/tests/unit/model-test.js +++ b/packages/-ember-data/tests/unit/model-test.js @@ -354,7 +354,7 @@ module('unit/model - Model', function (hooks) { // test .get access of id assert.strictEqual(person.get('id'), null, 'initial created model id should be null'); - store.setRecordId('odd-person', 'john', person._internalModel.clientId); + store._instanceCache.setRecordId('odd-person', 'john', person._internalModel.clientId); oddId = person.get('idComputed'); assert.strictEqual(oddId, 'john', 'computed get is correct'); diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index f8502dc58ed..78625fc1007 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -2389,8 +2389,8 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { assert.false(people.isDestroying, 'people is NOT destroying sync after unloadRecord'); assert.false(people.isDestroyed, 'people is NOT destroyed sync after unloadRecord'); - assert.true(peopleProxy.isDestroying, 'peopleProxy is destroying sync after unloadRecord'); - assert.true(peopleProxy.isDestroyed, 'peopleProxy is destroyed sync after unloadRecord'); + assert.false(peopleProxy.isDestroying, 'peopleProxy is NOT destroying sync after unloadRecord'); + assert.false(peopleProxy.isDestroyed, 'peopleProxy is NOT destroyed sync after unloadRecord'); }); assert.true(peopleProxy.isDestroying, 'peopleProxy is destroying after the run post unloadRecord'); diff --git a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js index 79eb2111c34..b3aff671f3d 100644 --- a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js @@ -74,11 +74,11 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let deferred = RSVP.defer(); const store = { - _query(modelName, query, array) { + query(modelName, query, options) { queryCalled++; assert.strictEqual(modelName, 'recordType'); assert.strictEqual(query, 'some-query'); - assert.strictEqual(array, recordArray); + assert.strictEqual(options._recordArray, recordArray); return deferred.promise; }, diff --git a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js index a672cb64cad..6f934aa5fac 100644 --- a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js @@ -173,9 +173,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { content, }); - let model1 = { lid: '@ember-data:lid-model-1' }; - let model2 = { lid: '@ember-data:lid-model-2' }; - let model3 = { lid: '@ember-data:lid-model-3' }; + let model1 = { lid: '@lid:model-1' }; + let model2 = { lid: '@lid:model-2' }; + let model3 = { lid: '@lid:model-3' }; assert.strictEqual(recordArray._pushIdentifiers([model1]), undefined, '_pushIdentifiers has no return value'); assert.deepEqual(recordArray.get('content'), [model1], 'now contains model1'); @@ -201,9 +201,9 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { content, }); - let model1 = { lid: '@ember-data:lid-model-1' }; - let model2 = { lid: '@ember-data:lid-model-2' }; - let model3 = { lid: '@ember-data:lid-model-3' }; + let model1 = { lid: '@lid:model-1' }; + let model2 = { lid: '@lid:model-2' }; + let model3 = { lid: '@lid:model-3' }; assert.strictEqual(recordArray.get('content').length, 0); assert.strictEqual(recordArray._removeIdentifiers([model1]), undefined, '_removeIdentifiers has no return value'); diff --git a/packages/-ember-data/tests/unit/store/adapter-interop-test.js b/packages/-ember-data/tests/unit/store/adapter-interop-test.js index 26d5a975084..4ce9424999d 100644 --- a/packages/-ember-data/tests/unit/store/adapter-interop-test.js +++ b/packages/-ember-data/tests/unit/store/adapter-interop-test.js @@ -691,75 +691,6 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); }); - test('store.fetchMany should always return a promise', function (assert) { - assert.expect(3); - - const Person = Model.extend(); - - this.owner.register('model:person', Person); - - let store = this.owner.lookup('service:store'); - - store.createRecord('person'); - - let records = []; - let results = run(() => store._scheduleFetchMany(records)); - - assert.ok(results, 'A call to store._scheduleFetchMany() should return a result'); - assert.ok(results.then, 'A call to store._scheduleFetchMany() should return a promise'); - - return results.then((returnedRecords) => { - assert.deepEqual(returnedRecords, [], 'The correct records are returned'); - }); - }); - - test('store._scheduleFetchMany should not resolve until all the records are resolved', async function (assert) { - assert.expect(1); - - const ApplicationAdapter = Adapter.extend({ - findRecord(store, type, id, snapshot) { - let record = { id, type: type.modelName }; - - return new EmberPromise((resolve) => { - later(() => resolve({ data: record }), 5); - }); - }, - - findMany(store, type, ids, snapshots) { - let records = ids.map((id) => ({ id, type: type.modelName })); - - return new EmberPromise((resolve) => { - later(() => { - resolve({ data: records }); - }, 15); - }); - }, - }); - - this.owner.register('model:test', Model.extend()); - this.owner.register('model:phone', Model.extend()); - this.owner.register('serializer:application', JSONAPISerializer.extend()); - this.owner.register('adapter:application', ApplicationAdapter); - - let store = this.owner.lookup('service:store'); - - store.createRecord('test'); - - let identifiers = [ - store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '10' }), - store.identifierCache.getOrCreateRecordIdentifier({ type: 'phone', id: '20' }), - store.identifierCache.getOrCreateRecordIdentifier({ type: 'phone', id: '21' }), - ]; - - await store._scheduleFetchMany(identifiers); - - const records = [store.peekRecord('test', '10'), store.peekRecord('phone', '20'), store.peekRecord('phone', '21')]; - - let unloadedRecords = records.filter((record) => record === null || record.isEmpty); - - assert.strictEqual(unloadedRecords.length, 0, 'All unloaded records should be loaded'); - }); - test('the store calls adapter.findMany according to groupings returned by adapter.groupRecordsForFindMany', async function (assert) { assert.expect(3); @@ -794,13 +725,13 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho store.identifierCache.getOrCreateRecordIdentifier({ type: 'test', id: '21' }), ]; - const result = await store._scheduleFetchMany(identifiers); + const result = await all(identifiers.map((id) => store.findRecord(id))); let ids = result.map((x) => x.id); assert.deepEqual(ids, ['10', '20', '21'], 'The promise fulfills with the identifiers'); }); - test('the promise returned by `_scheduleFetch`, when it resolves, does not depend on the promises returned to other calls to `_scheduleFetch` that are in the same run loop, but different groups', function (assert) { + test('the promise returned by `findRecord`, when it resolves, does not depend on the promises returned to other calls to `findRecord` that are in the same run loop, but different groups', function (assert) { assert.expect(2); let davidResolved = false; @@ -853,7 +784,7 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho }); }); - test('the promise returned by `_scheduleFetch`, when it rejects, does not depend on the promises returned to other calls to `_scheduleFetch` that are in the same run loop, but different groups', function (assert) { + 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', function (assert) { assert.expect(2); let davidResolved = false; diff --git a/packages/-ember-data/tests/unit/store/asserts-test.js b/packages/-ember-data/tests/unit/store/asserts-test.js index 8988e21dbf1..1958c70d758 100644 --- a/packages/-ember-data/tests/unit/store/asserts-test.js +++ b/packages/-ember-data/tests/unit/store/asserts-test.js @@ -28,7 +28,6 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'findAll', 'peekAll', 'modelFor', - '_modelFactoryFor', 'normalize', 'adapterFor', 'serializerFor', @@ -63,7 +62,6 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' 'peekAll', 'unloadAll', 'modelFor', - '_modelFactoryFor', 'push', '_push', 'pushPayload', @@ -84,7 +82,7 @@ module('unit/store/asserts - DS.Store methods produce useful assertion messages' }); }); - const STORE_TEARDOWN_METHODS = ['unloadAll', 'modelFor', '_modelFactoryFor']; + const STORE_TEARDOWN_METHODS = ['unloadAll', 'modelFor']; test('Calling Store teardown methods during destroy does not assert, but calling other methods does', function (assert) { store.shouldAssertMethodCallsOnDestroyedStore = true; diff --git a/packages/-ember-data/tests/unit/store/push-test.js b/packages/-ember-data/tests/unit/store/push-test.js index be984b9eb6f..1dc708498b6 100644 --- a/packages/-ember-data/tests/unit/store/push-test.js +++ b/packages/-ember-data/tests/unit/store/push-test.js @@ -1,6 +1,5 @@ import EmberObject from '@ember/object'; import { run } from '@ember/runloop'; -import Ember from 'ember'; import { module, test } from 'qunit'; import { resolve } from 'rsvp'; @@ -829,54 +828,6 @@ module('unit/store/push - DS.Store#push', function (hooks) { }, /must not be an array/); }); - testInDebug('Enabling Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS should warn on unknown attributes', function (assert) { - run(() => { - let originalFlagValue = Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS; - try { - Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS = true; - assert.expectWarning(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - firstName: 'Tomster', - emailAddress: 'tomster@emberjs.com', - isMascot: true, - }, - }, - }); - }, `The payload for 'person' contains these unknown attributes: emailAddress,isMascot. Make sure they've been defined in your model.`); - } finally { - Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS = originalFlagValue; - } - }); - }); - - testInDebug('Enabling Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS should warn on unknown relationships', function (assert) { - run(() => { - var originalFlagValue = Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS; - try { - Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS = true; - assert.expectWarning(() => { - store.push({ - data: { - type: 'person', - id: '1', - relationships: { - phoneNumbers: {}, - emailAddresses: {}, - mascots: {}, - }, - }, - }); - }, `The payload for 'person' contains these unknown relationships: emailAddresses,mascots. Make sure they've been defined in your model.`); - } finally { - Ember.ENV.DS_WARN_ON_UNKNOWN_KEYS = originalFlagValue; - } - }); - }); - testInDebug('Calling push with unknown keys should not warn by default', function (assert) { assert.expectNoWarning(() => { run(() => { diff --git a/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js b/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js index c16603254f7..90a8056e8a3 100644 --- a/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js +++ b/packages/-ember-data/tests/unit/system/relationships/polymorphic-relationship-payloads-test.js @@ -8,7 +8,7 @@ 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/system/relationships/relationship-payloads-manager (polymorphic)', function (hooks) { +module('unit/relationships/relationship-payloads-manager (polymorphic)', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { diff --git a/packages/-ember-data/yuidoc.json b/packages/-ember-data/yuidoc.json index 534649514ac..682b3823ca1 100644 --- a/packages/-ember-data/yuidoc.json +++ b/packages/-ember-data/yuidoc.json @@ -12,6 +12,7 @@ ], "paths": [ "addon", + "../../ember-data-types", "../adapter/addon", "../model/addon", "../serializer/addon", @@ -24,4 +25,4 @@ "exclude": "vendor", "outdir": "dist/docs" } -} \ No newline at end of file +} diff --git a/packages/adapter/addon/-private/build-url-mixin.ts b/packages/adapter/addon/-private/build-url-mixin.ts index aaea5508086..e7692e7fb99 100644 --- a/packages/adapter/addon/-private/build-url-mixin.ts +++ b/packages/adapter/addon/-private/build-url-mixin.ts @@ -3,9 +3,9 @@ import { camelize } from '@ember/string'; import { pluralize } from 'ember-inflector'; -import type Snapshot from '@ember-data/store/-private/system/snapshot'; -import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type Snapshot from '@ember-data/store/-private/snapshot'; +import type SnapshotRecordArray from '@ember-data/store/-private/snapshot-record-array'; +import type { Dict } from '@ember-data/types/q/utils'; /** @module @ember-data/adapter diff --git a/packages/adapter/addon/-private/utils/determine-body-promise.ts b/packages/adapter/addon/-private/utils/determine-body-promise.ts index 94ede475967..6c7e2a17035 100644 --- a/packages/adapter/addon/-private/utils/determine-body-promise.ts +++ b/packages/adapter/addon/-private/utils/determine-body-promise.ts @@ -1,7 +1,7 @@ import { warn } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { Dict } from '@ember-data/types/q/utils'; import type { RequestData } from '../../rest'; import continueOnReject from './continue-on-reject'; diff --git a/packages/adapter/addon/-private/utils/fetch.ts b/packages/adapter/addon/-private/utils/fetch.ts index a258d3a6beb..4751d62010d 100644 --- a/packages/adapter/addon/-private/utils/fetch.ts +++ b/packages/adapter/addon/-private/utils/fetch.ts @@ -11,7 +11,7 @@ export default function getFetchFunction(): FetchFunction { if (has('fetch')) { // use `fetch` module by default, this is commonly provided by ember-fetch - let fetchFn = require('fetch').default; + let fetchFn = (require('fetch') as { default: FetchFunction }).default; _fetch = () => fetchFn; } else if (typeof fetch === 'function') { // fallback to using global fetch diff --git a/packages/adapter/addon/-private/utils/parse-response-headers.ts b/packages/adapter/addon/-private/utils/parse-response-headers.ts index 27b3dcd6a6b..f98d84edd93 100644 --- a/packages/adapter/addon/-private/utils/parse-response-headers.ts +++ b/packages/adapter/addon/-private/utils/parse-response-headers.ts @@ -1,4 +1,4 @@ -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { Dict } from '@ember-data/types/q/utils'; const newline = /\r?\n/; diff --git a/packages/adapter/addon/-private/utils/serialize-into-hash.ts b/packages/adapter/addon/-private/utils/serialize-into-hash.ts index f5706b7f398..e82b8339753 100644 --- a/packages/adapter/addon/-private/utils/serialize-into-hash.ts +++ b/packages/adapter/addon/-private/utils/serialize-into-hash.ts @@ -3,9 +3,9 @@ import { assert } from '@ember/debug'; import type { Snapshot } from 'ember-data/-private'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; -import type { DSModelSchema } from '@ember-data/store/-private/ts-interfaces/ds-model'; -import type { MinimumSerializerInterface } from '@ember-data/store/-private/ts-interfaces/minimum-serializer-interface'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type { DSModelSchema } from '@ember-data/types/q/ds-model'; +import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; type SerializerWithSerializeIntoHash = MinimumSerializerInterface & { serializeIntoHash?(hash: {}, modelClass: ShimModelClass, snapshot: Snapshot, options?: { includeId?: boolean }): void; diff --git a/packages/adapter/addon/index.ts b/packages/adapter/addon/index.ts index 22a9e13d1b0..76ed5357b5b 100644 --- a/packages/adapter/addon/index.ts +++ b/packages/adapter/addon/index.ts @@ -143,13 +143,10 @@ import { Promise as RSVPPromise } from 'rsvp'; import type Store from '@ember-data/store'; import type { Snapshot } from '@ember-data/store/-private'; -import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; -import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; -import type { - AdapterPayload, - MinimumAdapterInterface, -} from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type SnapshotRecordArray from '@ember-data/store/-private/snapshot-record-array'; +import type { AdapterPayload, MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface'; +import type { Dict } from '@ember-data/types/q/utils'; /** An adapter is an object that receives requests from a store and diff --git a/packages/adapter/addon/json-api.ts b/packages/adapter/addon/json-api.ts index 5def84b16ec..9ddcc2d1c61 100644 --- a/packages/adapter/addon/json-api.ts +++ b/packages/adapter/addon/json-api.ts @@ -7,9 +7,9 @@ import { dasherize } from '@ember/string'; import { pluralize } from 'ember-inflector'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; -import type Snapshot from '@ember-data/store/-private/system/snapshot'; -import { AdapterPayload } from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type Snapshot from '@ember-data/store/-private/snapshot'; +import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; import { serializeIntoHash } from './-private'; import type { FetchRequestInit, JQueryRequestInit } from './rest'; diff --git a/packages/adapter/addon/rest.ts b/packages/adapter/addon/rest.ts index a774d1b4220..33e8cca63d8 100644 --- a/packages/adapter/addon/rest.ts +++ b/packages/adapter/addon/rest.ts @@ -10,11 +10,11 @@ import { DEBUG } from '@glimmer/env'; import { Promise as RSVPPromise } from 'rsvp'; import type Store from '@ember-data/store'; -import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; -import type Snapshot from '@ember-data/store/-private/system/snapshot'; -import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; -import { AdapterPayload } from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type Snapshot from '@ember-data/store/-private/snapshot'; +import type SnapshotRecordArray from '@ember-data/store/-private/snapshot-record-array'; +import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; +import type { Dict } from '@ember-data/types/q/utils'; import { determineBodyPromise, fetch, parseResponseHeaders, serializeIntoHash, serializeQueryParams } from './-private'; import type { FastBoot } from './-private/fastboot-interface'; diff --git a/packages/adapter/types/require/index.d.ts b/packages/adapter/types/require/index.d.ts deleted file mode 100644 index 447d7e28142..00000000000 --- a/packages/adapter/types/require/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function (moduleName: string): any; diff --git a/packages/debug/addon/index.js b/packages/debug/addon/index.js index 5902e991f27..a8a99fdd748 100644 --- a/packages/debug/addon/index.js +++ b/packages/debug/addon/index.js @@ -81,7 +81,7 @@ export default DataAdapter.extend({ */ watchModelTypes(typesAdded, typesUpdated) { const store = get(this, 'store'); - const __createRecordData = store._createRecordData; + const __createRecordData = store._instanceCache._createRecordData; const _releaseMethods = []; const discoveredTypes = typesMapFor(store); @@ -91,15 +91,15 @@ export default DataAdapter.extend({ }); // Overwrite _createRecordData so newly added models will get added to the list - store._createRecordData = (identifier) => { + store._instanceCache._createRecordData = (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 __createRecordData.call(store, identifier); + return __createRecordData.call(store._instanceCache, identifier); }; let release = () => { _releaseMethods.forEach((fn) => fn()); - store._createRecordData = __createRecordData; + store._instanceCache._createRecordData = __createRecordData; // 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 diff --git a/packages/model/addon/-private/belongs-to.js b/packages/model/addon/-private/belongs-to.js index 27f94370357..c501ad3cd89 100644 --- a/packages/model/addon/-private/belongs-to.js +++ b/packages/model/addon/-private/belongs-to.js @@ -2,6 +2,7 @@ import { assert, inspect, warn } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; +import { LEGACY_SUPPORT } from './model'; import { computedMacroWithOptionalParams } from './util'; /** @@ -142,6 +143,8 @@ function belongsTo(modelName, options) { return computed({ get(key) { + const support = LEGACY_SUPPORT.lookup(this); + if (DEBUG) { if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { throw new Error( @@ -150,7 +153,7 @@ function belongsTo(modelName, options) { } if (Object.prototype.hasOwnProperty.call(opts, 'serialize')) { warn( - `You provided a serialize option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See Serializer and it's implementations https://api.emberjs.com/ember-data/release/classes/Serializer`, + `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', @@ -160,7 +163,7 @@ function belongsTo(modelName, options) { if (Object.prototype.hasOwnProperty.call(opts, 'embedded')) { warn( - `You provided an embedded option on the "${key}" property in the "${this._internalModel.modelName}" class, this belongs in the serializer. See EmbeddedRecordsMixin https://api.emberjs.com/ember-data/release/classes/EmbeddedRecordsMixin`, + `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', @@ -169,9 +172,10 @@ function belongsTo(modelName, options) { } } - return this._internalModel.getBelongsTo(key); + return support.getBelongsTo(key); }, set(key, value) { + const support = LEGACY_SUPPORT.lookup(this); if (DEBUG) { if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { throw new Error( @@ -180,10 +184,10 @@ function belongsTo(modelName, options) { } } this.store._backburner.join(() => { - this._internalModel.setDirtyBelongsTo(key, value); + support.setDirtyBelongsTo(key, value); }); - return this._internalModel.getBelongsTo(key); + return support.getBelongsTo(key); }, }).meta(meta); } diff --git a/packages/model/addon/-private/system/diff-array.ts b/packages/model/addon/-private/diff-array.ts similarity index 100% rename from packages/model/addon/-private/system/diff-array.ts rename to packages/model/addon/-private/diff-array.ts diff --git a/packages/model/addon/-private/has-many.js b/packages/model/addon/-private/has-many.js index 66ef43fba6a..8b2ef4a1580 100644 --- a/packages/model/addon/-private/has-many.js +++ b/packages/model/addon/-private/has-many.js @@ -5,6 +5,7 @@ import { assert, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { DEBUG } from '@glimmer/env'; +import { LEGACY_SUPPORT } from './model'; import { computedMacroWithOptionalParams } from './util'; /** @@ -182,28 +183,28 @@ function hasMany(type, options) { return computed({ get(key) { if (DEBUG) { - if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { + if (['_internalModel', '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()}` ); } } - return this._internalModel.getHasMany(key); + return LEGACY_SUPPORT.lookup(this).getHasMany(key); }, set(key, records) { if (DEBUG) { - if (['_internalModel', 'recordData', 'currentState'].indexOf(key) !== -1) { + if (['_internalModel', '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()}` ); } } - let internalModel = this._internalModel; + const support = LEGACY_SUPPORT.lookup(this); this.store._backburner.join(() => { - internalModel.setDirtyHasMany(key, records); + support.setDirtyHasMany(key, records); }); - return internalModel.getHasMany(key); + return support.getHasMany(key); }, }).meta(meta); } diff --git a/packages/model/addon/-private/index.ts b/packages/model/addon/-private/index.ts index 68bd94971f1..2f6719f35e4 100644 --- a/packages/model/addon/-private/index.ts +++ b/packages/model/addon/-private/index.ts @@ -4,10 +4,11 @@ export { default as hasMany } from './has-many'; export { default as Model } from './model'; export { default as Errors } from './errors'; -export { default as ManyArray } from './system/many-array'; -export { default as PromiseBelongsTo } from './system/promise-belongs-to'; -export { default as PromiseManyArray } from './system/promise-many-array'; -export { default as _modelForMixin } from './system/model-for-mixin'; +export { default as ManyArray } from './many-array'; +export { default as PromiseBelongsTo } from './promise-belongs-to'; +export { default as PromiseManyArray } from './promise-many-array'; +export { default as _modelForMixin } from './model-for-mixin'; // // Used by tests -export { default as diffArray } from './system/diff-array'; +export { default as diffArray } from './diff-array'; +export { LEGACY_SUPPORT } from './model'; diff --git a/packages/store/addon/-private/system/store/finders.js b/packages/model/addon/-private/legacy-data-fetch.js similarity index 74% rename from packages/store/addon/-private/system/store/finders.js rename to packages/model/addon/-private/legacy-data-fetch.js index 7c1cff6e486..f9eea8f6c45 100644 --- a/packages/store/addon/-private/system/store/finders.js +++ b/packages/model/addon/-private/legacy-data-fetch.js @@ -1,31 +1,132 @@ import { assert, deprecate } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { Promise } from 'rsvp'; +import { resolve } from 'rsvp'; import { DEPRECATE_RSVP_PROMISE } from '@ember-data/private-build-infra/deprecations'; -import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common'; -import { normalizeResponseHelper } from './serializer-response'; +import { iterateData, normalizeResponseHelper } from './legacy-data-utils'; -/** - @module @ember-data/store -*/ +export function _findHasMany(adapter, store, identifier, link, relationship, options) { + const snapshot = store._instanceCache.createSnapshot(identifier, options); + let modelClass = store.modelFor(relationship.type); + let useLink = !link || typeof link === 'string'; + let relatedLink = useLink ? link : link.href; + let promise = adapter.findHasMany(store, snapshot, relatedLink, relationship); + let label = `DS: Handle Adapter#findHasMany of '${identifier.type}' : '${relationship.type}'`; -function payloadIsNotBlank(adapterPayload) { - if (Array.isArray(adapterPayload)) { - return true; - } else { - return Object.keys(adapterPayload || {}).length; + promise = guardDestroyedStore(promise, store, label); + promise = promise.then( + (adapterPayload) => { + if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { + 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) + ); + 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); + }, + null, + `DS: Extract payload of '${identifier.type}' : hasMany '${relationship.type}'` + ); + + if (DEPRECATE_RSVP_PROMISE) { + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); } + + return promise; } -function iterateData(data, fn) { - if (Array.isArray(data)) { - return data.map(fn); - } else { - return fn(data); +export function _findBelongsTo(store, identifier, link, relationship, options) { + 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._instanceCache.createSnapshot(identifier, options); + let modelClass = store.modelFor(relationship.type); + let useLink = !link || typeof link === 'string'; + let relatedLink = useLink ? link : link.href; + let promise = adapter.findBelongsTo(store, snapshot, relatedLink, relationship); + let label = `DS: Handle Adapter#findBelongsTo of ${identifier.type} : ${relationship.type}`; + + promise = guardDestroyedStore(promise, store, label); + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); + + promise = promise.then( + (adapterPayload) => { + if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { + 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', + }, + } + ); + } + } + + 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); + }, + null, + `DS: Extract payload of ${identifier.type} : ${relationship.type}` + ); + + if (DEPRECATE_RSVP_PROMISE) { + promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); } + + return promise; } // sync @@ -132,15 +233,15 @@ function getInverse(store, parentInternalModel, parentRelationship, type) { return recordDataFindInverseRelationshipInfo(store, parentInternalModel, parentRelationship, type); } -function recordDataFindInverseRelationshipInfo({ _storeWrapper }, parentIdentifier, parentRelationship, type) { +function recordDataFindInverseRelationshipInfo(store, parentIdentifier, parentRelationship, type) { let { name: lhs_relationshipName } = parentRelationship; let { type: parentType } = parentIdentifier; - let inverseKey = _storeWrapper.inverseForRelationship(parentType, lhs_relationshipName); + let inverseKey = store._instanceCache._storeWrapper.inverseForRelationship(parentType, lhs_relationshipName); if (inverseKey) { let { meta: { kind }, - } = _storeWrapper.relationshipsDefinitionFor(type)[inverseKey]; + } = store._instanceCache._storeWrapper.relationshipsDefinitionFor(type)[inverseKey]; return { inverseKey, kind, @@ -205,213 +306,65 @@ function validateRelationshipEntry({ id }, { id: parentModelID }) { return id && id.toString() === parentModelID; } -export function _findHasMany(adapter, store, identifier, link, relationship, options) { - const snapshot = store._instanceCache.createSnapshot(identifier, options); - let modelClass = store.modelFor(relationship.type); - let useLink = !link || typeof link === 'string'; - let relatedLink = useLink ? link : link.href; - let promise = adapter.findHasMany(store, snapshot, relatedLink, relationship); - let label = `DS: Handle Adapter#findHasMany of '${identifier.type}' : '${relationship.type}'`; - - promise = guardDestroyedStore(promise, store, label); - promise = promise.then( - (adapterPayload) => { - if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { - 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) - ); - 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); - }, - null, - `DS: Extract payload of '${identifier.type}' : hasMany '${relationship.type}'` - ); - - if (DEPRECATE_RSVP_PROMISE) { - promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); - } - - return promise; +function _bind(fn, ...args) { + return function () { + return fn.apply(undefined, args); + }; } -export function _findBelongsTo(store, identifier, link, relationship, options) { - 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._instanceCache.createSnapshot(identifier, options); - let modelClass = store.modelFor(relationship.type); - let useLink = !link || typeof link === 'string'; - let relatedLink = useLink ? link : link.href; - let promise = adapter.findBelongsTo(store, snapshot, relatedLink, relationship); - let label = `DS: Handle Adapter#findBelongsTo of ${identifier.type} : ${relationship.type}`; - - promise = guardDestroyedStore(promise, store, label); - promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); - - promise = promise.then( - (adapterPayload) => { - if (!_objectIsAlive(store._instanceCache.getInternalModel(identifier))) { - 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', - }, - } - ); - } - } - - 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); - }, - null, - `DS: Extract payload of ${identifier.type} : ${relationship.type}` - ); - - if (DEPRECATE_RSVP_PROMISE) { - promise = _guard(promise, _bind(_objectIsAlive, store._instanceCache.getInternalModel(identifier))); - } +function _guard(promise, test) { + let guarded = promise.finally(() => { + if (!test()) { + guarded._subscribers.length = 0; + } + }); - return promise; + return guarded; } -export function _findAll(adapter, store, modelName, options) { - let modelClass = store.modelFor(modelName); // adapter.findAll depends on the class - let recordArray = store.peekAll(modelName); - let snapshotArray = recordArray._createSnapshot(options); - let promise = Promise.resolve().then(() => adapter.findAll(store, modelClass, null, snapshotArray)); - let label = 'DS: Handle Adapter#findAll of ' + modelClass; - - promise = guardDestroyedStore(promise, store, label); - - return promise.then( - (adapterPayload) => { - assert( - `You made a 'findAll' request for '${modelName}' records, 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, 'findAll'); - - store._push(payload); - store.recordArrayManager._didUpdateAll(modelName); - - return recordArray; - }, - null, - 'DS: Extract payload of findAll ${modelName}' - ); +function _objectIsAlive(object) { + return !(object.isDestroyed || object.isDestroying); } -export function _query(adapter, store, modelName, query, recordArray, options) { - let modelClass = store.modelFor(modelName); // adapter.query needs the class - - recordArray = recordArray || store.recordArrayManager.createAdapterPopulatedRecordArray(modelName, query); - let promise = Promise.resolve().then(() => adapter.query(store, modelClass, query, recordArray, options)); - - let label = `DS: Handle Adapter#query of ${modelName}`; - promise = guardDestroyedStore(promise, store, label); - - return promise.then( - (adapterPayload) => { - let serializer = store.serializerFor(modelName); - let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'query'); - let identifiers = store._push(payload); +function payloadIsNotBlank(adapterPayload) { + if (Array.isArray(adapterPayload)) { + return true; + } else { + return Object.keys(adapterPayload || {}).length; + } +} - assert( - 'The response to store.query is expected to be an array but it was a single record. Please wrap your response in an array or use `store.queryRecord` to query for a single record.', - Array.isArray(identifiers) - ); - if (recordArray) { - recordArray._setIdentifiers(identifiers, payload); - } else { - recordArray = store.recordArrayManager.createAdapterPopulatedRecordArray( - modelName, - query, - identifiers, - payload +function guardDestroyedStore(promise, store, label) { + let token; + if (DEBUG) { + token = store._trackAsyncRequestStart(label); + } + let wrapperPromise = resolve(promise, label).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 recordArray; - }, - null, - `DS: Extract payload of query ${modelName}` - ); -} - -export function _queryRecord(adapter, store, modelName, query, options) { - let modelClass = store.modelFor(modelName); // adapter.queryRecord needs the class - let promise = Promise.resolve().then(() => adapter.queryRecord(store, modelClass, query, options)); - - let label = `DS: Handle Adapter#queryRecord of ${modelName}`; - promise = guardDestroyedStore(promise, store, label); - - return promise.then( - (adapterPayload) => { - let serializer = store.serializerFor(modelName); - let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'queryRecord'); - - assert( - `Expected the primary data returned by the serializer for a 'queryRecord' response to be a single object or null but instead it was an array.`, - !Array.isArray(payload.data) - ); + return promise; + }); - return store._push(payload); - }, - null, - `DS: Extract payload of queryRecord ${modelName}` - ); + return _guard(wrapperPromise, () => { + if (DEBUG) { + store._trackAsyncRequestEnd(token); + } + return _objectIsAlive(store); + }); } diff --git a/packages/model/addon/-private/legacy-data-utils.ts b/packages/model/addon/-private/legacy-data-utils.ts new file mode 100644 index 00000000000..f2191052546 --- /dev/null +++ b/packages/model/addon/-private/legacy-data-utils.ts @@ -0,0 +1,92 @@ +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; + +import type Store from '@ember-data/store'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type { JsonApiDocument } from '@ember-data/types/q/ember-data-json-api'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface'; +import type { MinimumSerializerInterface, RequestType } from '@ember-data/types/q/minimum-serializer-interface'; + +export function iterateData(data: T[] | T, fn: (o: T, index?: number) => T) { + if (Array.isArray(data)) { + return data.map(fn); + } else { + return fn(data); + } +} + +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 normalizeResponseHelper( + serializer: MinimumSerializerInterface | null, + store: Store, + modelClass: ShimModelClass, + payload: AdapterPayload, + id: string | null, + requestType: RequestType +): JsonApiDocument { + let normalizedResponse = serializer + ? serializer.normalizeResponse(store, modelClass, payload, id, requestType) + : payload; + + validateDocumentStructure(normalizedResponse); + + return normalizedResponse; +} + +export function validateDocumentStructure(doc?: AdapterPayload | JsonApiDocument): asserts doc is JsonApiDocument { + if (DEBUG) { + let errors: string[] = []; + if (!doc || typeof doc !== 'object') { + errors.push('Top level of a JSON API document must be an object'); + } else { + if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) { + errors.push('One or more of the following keys must be present: "data", "errors", "meta".'); + } else { + if ('data' in doc && 'errors' in doc) { + errors.push('Top level keys "errors" and "data" cannot both be present in a JSON API document'); + } + } + if ('data' in doc) { + if (!(doc.data === null || Array.isArray(doc.data) || typeof doc.data === 'object')) { + errors.push('data must be null, an object, or an array'); + } + } + if ('meta' in doc) { + if (typeof doc.meta !== 'object') { + errors.push('meta must be an object'); + } + } + if ('errors' in doc) { + if (!Array.isArray(doc.errors)) { + errors.push('errors must be an array'); + } + } + if ('links' in doc) { + if (typeof doc.links !== 'object') { + errors.push('links must be an object'); + } + } + if ('jsonapi' in doc) { + if (typeof doc.jsonapi !== 'object') { + errors.push('jsonapi must be an object'); + } + } + if ('included' in doc) { + if (typeof doc.included !== 'object') { + errors.push('included must be an array'); + } + } + } + + assert( + `Response must be normalized to a valid JSON API document:\n\t* ${errors.join('\n\t* ')}`, + errors.length === 0 + ); + } +} diff --git a/packages/model/addon/-private/legacy-relationships-support.ts b/packages/model/addon/-private/legacy-relationships-support.ts new file mode 100644 index 00000000000..672dbd66331 --- /dev/null +++ b/packages/model/addon/-private/legacy-relationships-support.ts @@ -0,0 +1,709 @@ +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; + +import { importSync } from '@embroider/macros'; +import { all, resolve } from 'rsvp'; + +import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; +import type { + BelongsToRelationship, + ManyRelationship, + RecordData as DefaultRecordData, +} from '@ember-data/record-data/-private'; +import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition'; +import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; +import type Store from '@ember-data/store'; +import type { InternalModel } from '@ember-data/store/-private'; +import { recordDataFor, recordIdentifierFor, storeFor } from '@ember-data/store/-private'; +import type { IdentifierCache } from '@ember-data/store/-private/identifier-cache'; +import type { DSModel } from '@ember-data/types/q/ds-model'; +import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { JsonApiRelationship } 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 { DefaultSingleResourceRelationship } from '@ember-data/types/q/relationship-record-data'; +import type { FindOptions } from '@ember-data/types/q/store'; +import type { Dict } from '@ember-data/types/q/utils'; + +import { _findBelongsTo, _findHasMany } from './legacy-data-fetch'; +import { assertIdentifierHasId } from './legacy-data-utils'; +import type { ManyArrayCreateArgs } from './many-array'; +import ManyArray from './many-array'; +import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to'; +import PromiseBelongsTo from './promise-belongs-to'; +import type { HasManyProxyCreateArgs } from './promise-many-array'; +import PromiseManyArray from './promise-many-array'; +import BelongsToReference from './references/belongs-to'; +import HasManyReference from './references/has-many'; + +type ManyArrayFactory = { create(args: ManyArrayCreateArgs): ManyArray }; +type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; + +export class LegacySupport { + declare record: DSModel; + declare store: Store; + declare recordData: DefaultRecordData; + declare references: Dict; + declare identifier: StableRecordIdentifier; + declare _manyArrayCache: Dict; + declare _relationshipPromisesCache: Dict>; + declare _relationshipProxyCache: Dict; + + declare isDestroying: boolean; + declare isDestroyed: boolean; + + constructor(record: DSModel) { + this.record = record; + this.store = storeFor(record)!; + this.identifier = recordIdentifierFor(record); + this.recordData = this.store._instanceCache.getRecordData(this.identifier) as DefaultRecordData; + + this._manyArrayCache = Object.create(null) as Dict; + this._relationshipPromisesCache = Object.create(null) as Dict>; + this._relationshipProxyCache = Object.create(null) as Dict; + this.references = Object.create(null) as Dict; + } + + _findBelongsTo( + key: string, + resource: DefaultSingleResourceRelationship, + relationshipMeta: RelationshipSchema, + options?: FindOptions + ): 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, relationshipMeta, options).then( + (identifier: StableRecordIdentifier | null) => + handleCompletedRelationshipRequest(this, key, resource._relationship, identifier), + (e: Error) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e) + ); + } + + reloadBelongsTo(key: string, options?: FindOptions): Promise { + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + if (loadingPromise) { + return loadingPromise; + } + + let resource = this.recordData.getBelongsTo(key); + // TODO move this to a public api + if (resource._relationship) { + resource._relationship.state.hasFailedLoadAttempt = false; + resource._relationship.state.shouldForceReload = true; + } + let relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier)[key]; + assert(`Attempted to reload a belongsTo relationship but no definition exists for it`, relationshipMeta); + let promise = this._findBelongsTo(key, resource, relationshipMeta, options); + if (this._relationshipProxyCache[key]) { + return this._updatePromiseProxyFor('belongsTo', key, { promise }); + } + return promise; + } + + getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null { + const { identifier, recordData } = this; + let resource = recordData.getBelongsTo(key); + let relatedIdentifier = + resource && resource.data ? this.store.identifierCache.getOrCreateRecordIdentifier(resource.data) : null; + let relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key]; + assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta); + + let store = this.store; + let async = relationshipMeta.options.async; + let isAsync = typeof async === 'undefined' ? true : async; + let _belongsToState: BelongsToProxyMeta = { + key, + store, + legacySupport: this, + modelName: relationshipMeta.type, + }; + + if (isAsync) { + if (resource._relationship.state.hasFailedLoadAttempt) { + return this._relationshipProxyCache[key] as PromiseBelongsTo; + } + + let promise = this._findBelongsTo(key, resource, relationshipMeta, options); + + return this._updatePromiseProxyFor('belongsTo', key, { + promise, + content: relatedIdentifier ? store._instanceCache.getRecord(relatedIdentifier) : null, + _belongsToState, + }); + } else { + if (relatedIdentifier === null) { + return null; + } else { + let toReturn = store._instanceCache.getRecord(relatedIdentifier); + assert( + `You looked up the '${key}' relationship on a '${identifier.type}' with id ${ + identifier.id || 'null' + } but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (\`belongsTo({ async: true })\`)`, + toReturn === null || !store._instanceCache.getInternalModel(relatedIdentifier).isEmpty + ); + return toReturn; + } + } + } + + setDirtyBelongsTo(key: string, value: RecordInstance | null) { + return this.recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value)); + } + + getManyArray(key: string, definition?: UpgradedMeta): ManyArray { + assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE); + let manyArray: ManyArray | undefined = this._manyArrayCache[key]; + if (!definition) { + const graphFor = ( + importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') + ).graphFor; + definition = graphFor(this.store).get(this.identifier, key).definition; + } + + if (!manyArray) { + manyArray = (ManyArray as unknown as ManyArrayFactory).create({ + store: this.store, + type: this.store.modelFor(definition.type), + recordData: this.recordData, + key, + isPolymorphic: definition.isPolymorphic, + isAsync: definition.isAsync, + _inverseIsAsync: definition.inverseIsAsync, + legacySupport: this, + isLoaded: !definition.isAsync, + }); + this._manyArrayCache[key] = manyArray; + } + + return manyArray; + } + + fetchAsyncHasMany( + key: string, + relationship: ManyRelationship, + manyArray: ManyArray, + options?: FindOptions + ): Promise { + if (HAS_RECORD_DATA_PACKAGE) { + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + if (loadingPromise) { + return loadingPromise; + } + + const jsonApi = this.recordData.getHasMany(key); + + loadingPromise = this._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options).then( + () => handleCompletedRelationshipRequest(this, key, relationship, manyArray), + (e: Error) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e) + ); + this._relationshipPromisesCache[key] = loadingPromise; + return loadingPromise; + } + assert('hasMany only works with the @ember-data/record-data package'); + } + + reloadHasMany(key: string, options?: FindOptions) { + if (HAS_RECORD_DATA_PACKAGE) { + let loadingPromise = this._relationshipPromisesCache[key]; + if (loadingPromise) { + return loadingPromise; + } + const graphFor = ( + importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') + ).graphFor; + const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + const { definition, state } = relationship; + + state.hasFailedLoadAttempt = false; + state.shouldForceReload = true; + let manyArray = this.getManyArray(key, definition); + let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); + + if (this._relationshipProxyCache[key]) { + return this._updatePromiseProxyFor('hasMany', key, { promise }); + } + + return promise; + } + assert(`hasMany only works with the @ember-data/record-data package`); + } + + getHasMany(key: string, options?: FindOptions): PromiseManyArray | ManyArray { + if (HAS_RECORD_DATA_PACKAGE) { + const graphFor = ( + importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') + ).graphFor; + const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + const { definition, state } = relationship; + let 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); + + return this._updatePromiseProxyFor('hasMany', key, { promise, content: manyArray }); + } else { + assert( + `You looked up the '${key}' relationship on a '${this.identifier.type}' with id ${ + this.identifier.id || 'null' + } but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async ('hasMany({ async: true })')`, + !anyUnloaded(this.store, relationship) + ); + + return manyArray; + } + } + assert(`hasMany only works with the @ember-data/record-data package`); + } + + setDirtyHasMany(key: string, records: RecordInstance[]) { + assertRecordsPassedToHasMany(records); + return this.recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records)); + } + + _updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray; + _updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo; + _updatePromiseProxyFor( + kind: 'belongsTo', + key: string, + args: { promise: Promise } + ): PromiseBelongsTo; + _updatePromiseProxyFor( + kind: 'hasMany' | 'belongsTo', + key: string, + args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } + ): PromiseBelongsTo | PromiseManyArray { + let promiseProxy = this._relationshipProxyCache[key]; + if (kind === 'hasMany') { + const { promise, content } = args as HasManyProxyCreateArgs; + if (promiseProxy) { + assert(`Expected a PromiseManyArray`, '_update' in promiseProxy); + promiseProxy._update(promise, content); + } else { + promiseProxy = this._relationshipProxyCache[key] = new PromiseManyArray(promise, content); + } + return promiseProxy; + } + if (promiseProxy) { + const { promise, content } = args as BelongsToProxyCreateArgs; + assert(`Expected a PromiseBelongsTo`, '_belongsToState' in promiseProxy); + + if (content !== undefined) { + promiseProxy.set('content', content); + } + void promiseProxy.set('promise', promise); + } else { + promiseProxy = (PromiseBelongsTo as unknown as PromiseBelongsToFactory).create(args as BelongsToProxyCreateArgs); + this._relationshipProxyCache[key] = promiseProxy; + } + + return promiseProxy; + } + + referenceFor(kind: string | null, name: string) { + let reference = this.references[name]; + + if (!reference) { + if (!HAS_RECORD_DATA_PACKAGE) { + // 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/record-data`); + } + const graphFor = ( + importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') + ).graphFor; + const relationship = graphFor(this.store).get(this.identifier, name); + + if (DEBUG && kind) { + let modelName = this.identifier.type; + let 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 + ); + } + + let relationshipKind = relationship.definition.kind; + + if (relationshipKind === 'belongsTo') { + reference = new BelongsToReference(this.store, this.identifier, relationship as BelongsToRelationship, name); + } else if (relationshipKind === 'hasMany') { + reference = new HasManyReference(this.store, this.identifier, relationship as ManyRelationship, name); + } + + this.references[name] = reference; + } + + return reference; + } + + _findHasManyByJsonApiResource( + resource, + parentIdentifier: StableRecordIdentifier, + relationship: ManyRelationship, + options: FindOptions = {} + ): Promise { + if (HAS_RECORD_DATA_PACKAGE) { + if (!resource) { + return resolve(); + } + const { definition, state } = relationship; + let adapter = this.store.adapterFor(definition.type); + + let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state; + const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource); + + let shouldFindViaLink = + resource.links && + resource.links.related && + (typeof adapter.findHasMany === 'function' || typeof resource.data === 'undefined') && + (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); + + // fetch via link + if (shouldFindViaLink) { + // findHasMany, although not public, does not need to care about our upgrade relationship definitions + // and can stick with the public definition API for now. + const relationshipMeta = this.store._instanceCache._storeWrapper.relationshipsDefinitionFor( + definition.inverseType + )[definition.key]; + let adapter = this.store.adapterFor(parentIdentifier.type); + + /* + If a relationship was originally populated by the adapter as a link + (as opposed to a list of IDs), this method is called when the + relationship is fetched. + + The link (which is usually a URL) is passed through unchanged, so the + adapter can make whatever request it wants. + + The usual use-case is for the server to register a URL as a link, and + then use that URL in the future to make a request for the relationship. + */ + assert( + `You tried to load a hasMany relationship but you have no adapter (for ${parentIdentifier.type})`, + adapter + ); + assert( + `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' + ); + + return _findHasMany(adapter, this.store, parentIdentifier, resource.links.related, relationshipMeta, options); + } + + let preferLocalCache = hasReceivedData && !isEmpty; + + let hasLocalPartialData = + hasDematerializedInverse || (isEmpty && Array.isArray(resource.data) && resource.data.length > 0); + + // fetch using data, pulling from local cache if possible + if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { + let finds = new Array(resource.data.length); + for (let i = 0; i < resource.data.length; i++) { + let identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data[i]); + finds[i] = this.store._instanceCache._fetchDataIfNeededForIdentifier(identifier, options); + } + + return all(finds); + } + + let hasData = hasReceivedData && !isEmpty; + + // fetch by data + if (hasData || hasLocalPartialData) { + let identifiers = resource.data.map((json) => this.store.identifierCache.getOrCreateRecordIdentifier(json)); + let fetches = new Array(identifiers.length); + const manager = this.store._fetchManager; + + for (let i = 0; i < identifiers.length; i++) { + let identifier = identifiers[i]; + assertIdentifierHasId(identifier); + fetches[i] = manager.scheduleFetch(identifier, options); + } + + return all(fetches); + } + + // we were explicitly told we have no data and no links. + // TODO if the relationshipIsStale, should we hit the adapter anyway? + return resolve(); + } + assert(`hasMany only works with the @ember-data/record-data package`); + } + + _findBelongsToByJsonApiResource( + resource, + parentIdentifier: StableRecordIdentifier, + relationshipMeta, + options: FindOptions = {} + ): Promise { + if (!resource) { + return resolve(null); + } + + const internalModel = resource.data ? this.store._instanceCache._internalModelForResource(resource.data) : null; + + let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship + .state as RelationshipState; + const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource); + + let shouldFindViaLink = + resource.links && + resource.links.related && + (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); + + if (internalModel) { + // short circuit if we are already loading + let pendingRequest = this.store._fetchManager.getPendingFetch(internalModel.identifier, options); + if (pendingRequest) { + return pendingRequest; + } + } + + // fetch via link + if (shouldFindViaLink) { + return _findBelongsTo(this.store, parentIdentifier, resource.links.related, relationshipMeta, options); + } + + let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; + let hasLocalPartialData = hasDematerializedInverse || (isEmpty && resource.data); + // null is explicit empty, undefined is "we don't know anything" + let localDataIsEmpty = resource.data === undefined || resource.data === null; + + // fetch using data, pulling from local cache if possible + if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { + /* + We have canonical data, but our local state is empty + */ + if (localDataIsEmpty) { + return resolve(null); + } + + if (!internalModel) { + assert(`No InternalModel found for ${resource.lid}`, internalModel); + } + + return this.store._instanceCache._fetchDataIfNeededForIdentifier(internalModel.identifier, options); + } + + let resourceIsLocal = !localDataIsEmpty && resource.data.id === null; + + if (internalModel && resourceIsLocal) { + return resolve(internalModel.identifier); + } + + // fetch by data + if (internalModel && !localDataIsEmpty) { + let identifier = internalModel.identifier; + assertIdentifierHasId(identifier); + + return this.store._fetchManager.scheduleFetch(identifier, options); + } + + // we were explicitly told we have no data and no links. + // TODO if the relationshipIsStale, should we hit the adapter anyway? + return resolve(null); + } + + destroy() { + assert( + 'Cannot destroy an internalModel while its record is materialized', + !this.record || this.record.isDestroyed || this.record.isDestroying + ); + this.isDestroying = true; + + const cache = this._manyArrayCache; + Object.keys(cache).forEach((key) => { + cache[key]!.destroy(); + delete cache[key]; + }); + const keys = Object.keys(this._relationshipProxyCache); + keys.forEach((key) => { + const proxy = this._relationshipProxyCache[key]!; + if (proxy.destroy) { + proxy.destroy(); + } + delete this._relationshipProxyCache[key]; + }); + if (this.references) { + const refs = this.references; + Object.keys(refs).forEach((key) => { + refs[key]!.destroy(); + delete refs[key]; + }); + } + this.isDestroyed = true; + } +} + +function handleCompletedRelationshipRequest( + recordExt: LegacySupport, + key: string, + relationship: BelongsToRelationship, + value: StableRecordIdentifier | null +): RecordInstance | null; +function handleCompletedRelationshipRequest( + recordExt: LegacySupport, + key: string, + relationship: ManyRelationship, + value: ManyArray +): ManyArray; +function handleCompletedRelationshipRequest( + recordExt: LegacySupport, + key: string, + relationship: BelongsToRelationship, + value: null, + error: Error +): never; +function handleCompletedRelationshipRequest( + recordExt: LegacySupport, + key: string, + relationship: ManyRelationship, + value: ManyArray, + error: Error +): never; +function handleCompletedRelationshipRequest( + recordExt: LegacySupport, + key: string, + relationship: BelongsToRelationship | ManyRelationship, + value: ManyArray | StableRecordIdentifier | null, + error?: Error +): ManyArray | RecordInstance | null { + delete recordExt._relationshipPromisesCache[key]; + relationship.state.shouldForceReload = false; + const isHasMany = relationship.definition.kind === 'hasMany'; + + if (isHasMany) { + // we don't notify the record property here to avoid refetch + // only the many array + (value as ManyArray).notify(); + } + + if (error) { + relationship.state.hasFailedLoadAttempt = true; + let 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 + // to destroyed content + // for the sync belongsTo reload case there will be no proxy + // 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 + if (proxy.content && proxy.content.isDestroying) { + (proxy as PromiseBelongsTo).set('content', null); + } + } + + throw error; + } + + if (isHasMany) { + (value as ManyArray).set('isLoaded', true); + } + + relationship.state.hasFailedLoadAttempt = false; + // only set to not stale if no error is thrown + relationship.state.isStale = false; + + return isHasMany || !value + ? (value as ManyArray | null) + : recordExt.store.peekRecord(value as StableRecordIdentifier); +} + +function assertRecordsPassedToHasMany(records: RecordInstance[]) { + 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 + .map((r) => `${typeof r}`) + .join(', ')}`, + (function () { + return records.every((record) => Object.prototype.hasOwnProperty.call(record, '_internalModel') === true); + })() + ); +} + +function extractRecordDatasFromRecords(records: RecordInstance[]): RecordData[] { + return records.map(extractRecordDataFromRecord) as RecordData[]; +} + +type PromiseProxyRecord = { then(): void; get(str: 'content'): RecordInstance | null | undefined }; + +function extractRecordDataFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { + if (!recordOrPromiseRecord) { + return null; + } + + if (isPromiseRecord(recordOrPromiseRecord)) { + let content = recordOrPromiseRecord.get && recordOrPromiseRecord.get('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 + ); + return content ? recordDataFor(content) : null; + } + + return recordDataFor(recordOrPromiseRecord); +} + +function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { + return !!record.then; +} + +function anyUnloaded(store: Store, relationship: ManyRelationship) { + let state = relationship.currentState; + const unloaded = state.find((s) => { + let im = store._instanceCache.getInternalModel(s); + return im._isDematerializing || !im.isLoaded; + }); + + return unloaded || false; +} + +/** + * Flag indicating whether all inverse records are available + * + * true if the inverse exists and is loaded (not empty) + * true if there is no inverse + * false if the inverse exists and is not loaded (empty) + * + * @internal + * @return {boolean} + */ +function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelationship): boolean { + const cache = store.identifierCache; + + if (Array.isArray(resource.data)) { + // treat as collection + // check for unloaded records + let hasEmptyRecords = resource.data.reduce((hasEmptyModel, resourceIdentifier) => { + return hasEmptyModel || internalModelForRelatedResource(store, cache, resourceIdentifier).isEmpty; + }, false); + + return !hasEmptyRecords; + } else { + // treat as single resource + if (!resource.data) { + return true; + } else { + const internalModel = internalModelForRelatedResource(store, cache, resource.data); + return !internalModel.isEmpty; + } + } +} + +function internalModelForRelatedResource( + store: Store, + cache: IdentifierCache, + resource: ResourceIdentifierObject +): InternalModel { + const identifier = cache.getOrCreateRecordIdentifier(resource); + return store._instanceCache._internalModelForResource(identifier); +} diff --git a/packages/model/addon/-private/system/many-array.ts b/packages/model/addon/-private/many-array.ts similarity index 90% rename from packages/model/addon/-private/system/many-array.ts rename to packages/model/addon/-private/many-array.ts index 5f9c34ad214..3b1dc623cd2 100644 --- a/packages/model/addon/-private/system/many-array.ts +++ b/packages/model/addon/-private/many-array.ts @@ -8,19 +8,20 @@ import EmberObject, { get } from '@ember/object'; import { all } from 'rsvp'; -import type { RelationshipRecordData } from '@ember-data/record-data/-private/ts-interfaces/relationship-record-data'; -import type { InternalModel } from '@ember-data/store/-private'; +import type Store from '@ember-data/store'; import { PromiseArray, recordDataFor } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { CreateRecordProperties } from '@ember-data/store/-private/system/core-store'; -import ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; -import type { DSModelSchema } from '@ember-data/store/-private/ts-interfaces/ds-model'; -import type { Links, PaginationLinks } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { CreateRecordProperties } from '@ember-data/store/-private/core-store'; +import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class'; +import type { DSModelSchema } from '@ember-data/types/q/ds-model'; +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 { RelationshipRecordData } from '@ember-data/types/q/relationship-record-data'; +import type { FindOptions } from '@ember-data/types/q/store'; +import type { Dict } from '@ember-data/types/q/utils'; import diffArray from './diff-array'; +import { LegacySupport } from './legacy-relationships-support'; interface MutableArrayWithObject extends EmberObject, MutableArray {} const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as new < @@ -29,14 +30,14 @@ const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as ne >() => MutableArrayWithObject; export interface ManyArrayCreateArgs { - store: CoreStore; + store: Store; type: ShimModelClass; recordData: RelationshipRecordData; key: string; isPolymorphic: boolean; isAsync: boolean; _inverseIsAsync: boolean; - internalModel: InternalModel; + legacySupport: LegacySupport; isLoaded: boolean; } /** @@ -98,8 +99,8 @@ export default class ManyArray extends MutableArrayWithObject { + const identifier = recordIdentifierFor(record); + let support = LEGACY_SUPPORT.get(identifier); + + if (!support) { + support = new LegacySupport(record); + LEGACY_SUPPORT.set(identifier, support); + } + return support; +}; function findPossibleInverses(type, inverseType, name, relationshipsSoFar) { let possibleRelationships = relationshipsSoFar || []; @@ -138,6 +151,10 @@ class Model extends EmberObject { }); } + willDestroy() { + LEGACY_SUPPORT.get(this)?.destroy(); + } + /** If this property is `true` the record is in the `empty` state. Empty is the first state all records enter after they have @@ -1020,7 +1037,7 @@ class Model extends EmberObject { @return {BelongsToReference} reference for this relationship */ belongsTo(name) { - return this._internalModel.referenceFor('belongsTo', name); + return LEGACY_SUPPORT.lookup(this).referenceFor('belongsTo', name); } /** @@ -1083,7 +1100,7 @@ class Model extends EmberObject { @return {HasManyReference} reference for this relationship */ hasMany(name) { - return this._internalModel.referenceFor('hasMany', name); + return LEGACY_SUPPORT.lookup(this).referenceFor('hasMany', name); } /** diff --git a/packages/model/addon/-private/notify-changes.ts b/packages/model/addon/-private/notify-changes.ts index d5520eb06fb..109a6b94825 100644 --- a/packages/model/addon/-private/notify-changes.ts +++ b/packages/model/addon/-private/notify-changes.ts @@ -1,17 +1,18 @@ import { cacheFor } from '@ember/object/internals'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { NotificationType } from '@ember-data/store/-private/system/record-notification-manager'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type Store from '@ember-data/store'; +import type { NotificationType } from '@ember-data/store/-private/record-notification-manager'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -type Model = InstanceType; +import type Model from './model'; +import { LEGACY_SUPPORT } from './model'; export default function notifyChanges( identifier: StableRecordIdentifier, value: NotificationType, key: string | undefined, record: Model, - store: CoreStore + store: Store ) { if (value === 'attributes') { if (key) { @@ -24,10 +25,10 @@ export default function notifyChanges( } else if (value === 'relationships') { if (key) { let meta = record.constructor.relationshipsByName.get(key); - notifyRelationship(store, identifier, key, record, meta); + notifyRelationship(identifier, key, record, meta); } else { record.eachRelationship((key, meta) => { - notifyRelationship(store, identifier, key, record, meta); + notifyRelationship(identifier, key, record, meta); }); } } else if (value === 'identity') { @@ -35,12 +36,19 @@ export default function notifyChanges( } } -function notifyRelationship(store: CoreStore, identifier: StableRecordIdentifier, key: string, record: Model, meta) { - let internalModel = store._internalModelForResource(identifier); +function notifyRelationship(identifier: StableRecordIdentifier, key: string, record: Model, meta) { if (meta.kind === 'belongsTo') { record.notifyPropertyChange(key); } else if (meta.kind === 'hasMany') { - let manyArray = internalModel._manyArrayCache[key]; + let support = LEGACY_SUPPORT.get(identifier); + let manyArray = support && support._manyArrayCache[key]; + let hasPromise = support && support._relationshipPromisesCache[key]; + + if (manyArray && hasPromise) { + // do nothing, we will notify the ManyArray directly + // once the fetch has completed. + return; + } if (manyArray) { manyArray.notify(); @@ -55,7 +63,7 @@ function notifyRelationship(store: CoreStore, identifier: StableRecordIdentifier } } -function notifyAttribute(store: CoreStore, identifier: StableRecordIdentifier, key: string, record: Model) { +function notifyAttribute(store: Store, identifier: StableRecordIdentifier, key: string, record: Model) { let currentValue = cacheFor(record, key); if (currentValue !== store._instanceCache.getRecordData(identifier).getAttr(key)) { diff --git a/packages/model/addon/-private/system/promise-belongs-to.ts b/packages/model/addon/-private/promise-belongs-to.ts similarity index 81% rename from packages/model/addon/-private/system/promise-belongs-to.ts rename to packages/model/addon/-private/promise-belongs-to.ts index 4d944f7fd1d..bf9dc9a8a0e 100644 --- a/packages/model/addon/-private/system/promise-belongs-to.ts +++ b/packages/model/addon/-private/promise-belongs-to.ts @@ -3,16 +3,17 @@ import { computed } from '@ember/object'; import type PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import type ObjectProxy from '@ember/object/proxy'; -import type { InternalModel } from '@ember-data/store/-private'; +import type Store from '@ember-data/store'; import { PromiseObject } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { Dict } from '@ember-data/types/q/utils'; + +import { LegacySupport } from './legacy-relationships-support'; export interface BelongsToProxyMeta { key: string; - store: CoreStore; - originatingInternalModel: InternalModel; + store: Store; + legacySupport: LegacySupport; modelName: string; } export interface BelongsToProxyCreateArgs { @@ -63,8 +64,8 @@ class PromiseBelongsTo extends Extended { async reload(options: Dict): Promise { assert('You are trying to reload an async belongsTo before it has been created', this.content !== undefined); - let { key, originatingInternalModel } = this._belongsToState; - await originatingInternalModel.reloadBelongsTo(key, options); + let { key, legacySupport } = this._belongsToState; + await legacySupport.reloadBelongsTo(key, options); return this; } } diff --git a/packages/model/addon/-private/system/promise-many-array.ts b/packages/model/addon/-private/promise-many-array.ts similarity index 98% rename from packages/model/addon/-private/system/promise-many-array.ts rename to packages/model/addon/-private/promise-many-array.ts index 3e2cdb045cd..da91a3739b0 100644 --- a/packages/model/addon/-private/system/promise-many-array.ts +++ b/packages/model/addon/-private/promise-many-array.ts @@ -10,7 +10,7 @@ import { resolve } from 'rsvp'; import type { ManyArray } from 'ember-data/-private'; import type { InternalModel } from '@ember-data/store/-private'; -import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; export interface HasManyProxyCreateArgs { promise: Promise; diff --git a/packages/model/addon/-private/record-state.ts b/packages/model/addon/-private/record-state.ts index 1df6455b5bc..37434f920f5 100644 --- a/packages/model/addon/-private/record-state.ts +++ b/packages/model/addon/-private/record-state.ts @@ -3,13 +3,13 @@ import { dependentKeyCompat } from '@ember/object/compat'; import { DEBUG } from '@glimmer/env'; import { cached, tracked } from '@glimmer/tracking'; +import type Store from '@ember-data/store'; import { storeFor } from '@ember-data/store'; import { errorsArrayToHash, recordIdentifierFor } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { NotificationType } from '@ember-data/store/-private/system/record-notification-manager'; -import type RequestCache from '@ember-data/store/-private/system/request-cache'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; +import type { NotificationType } from '@ember-data/store/-private/record-notification-manager'; +import type RequestCache from '@ember-data/store/-private/request-cache'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; type Model = InstanceType; @@ -139,7 +139,7 @@ root @internal */ export default class RecordState { - declare store: CoreStore; + declare store: Store; declare identifier: StableRecordIdentifier; declare record: Model; declare rs: RequestCache; diff --git a/packages/store/addon/-private/system/references/belongs-to.ts b/packages/model/addon/-private/references/belongs-to.ts similarity index 60% rename from packages/store/addon/-private/system/references/belongs-to.ts rename to packages/model/addon/-private/references/belongs-to.ts index 15f6b1ee9a6..e2762b0587e 100644 --- a/packages/store/addon/-private/system/references/belongs-to.ts +++ b/packages/model/addon/-private/references/belongs-to.ts @@ -1,24 +1,45 @@ import { dependentKeyCompat } from '@ember/object/compat'; import { cached, tracked } from '@glimmer/tracking'; +import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; import { resolve } from 'rsvp'; import type { BelongsToRelationship } from '@ember-data/record-data/-private'; +import type Store from '@ember-data/store'; import { assertPolymorphicType } from '@ember-data/store/-debug'; - -import { SingleResourceDocument } from '../../ts-interfaces/ember-data-json-api'; -import { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import { RecordInstance } from '../../ts-interfaces/record-instance'; -import CoreStore from '../core-store'; -import { NotificationType, unsubscribe } from '../record-notification-manager'; -import { internalModelFactoryFor, recordIdentifierFor } from '../store/internal-model-factory'; -import RecordReference from './record'; -import Reference from './reference'; +import { recordIdentifierFor } from '@ember-data/store/-private'; +import type { NotificationType } from '@ember-data/store/-private/record-notification-manager'; +import type { DebugWeakCache } from '@ember-data/store/-private/weak-cache'; +import type { + LinkObject, + Links, + 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'; + +import type { LegacySupport } from '../legacy-relationships-support'; +import { LEGACY_SUPPORT } from '../model'; /** - @module @ember-data/store + @module @ember-data/model */ +interface ResourceIdentifier { + links?: { + related?: string | LinkObject; + }; + meta?: JSONObject; +} + +function isResourceIdentiferWithRelatedLinks( + value: SingleResourceRelationship | ResourceIdentifier | null +): value is ResourceIdentifier & { links: { related: string | LinkObject | null } } { + return Boolean(value && value.links && value.links.related); +} + /** A `BelongsToReference` is a low-level API that allows users and addon authors to perform meta-operations on a belongs-to @@ -26,14 +47,13 @@ import Reference from './reference'; @class BelongsToReference @public - @extends Reference */ -export default class BelongsToReference extends Reference { +export default class BelongsToReference { declare key: string; declare belongsToRelationship: BelongsToRelationship; declare type: string; - declare parent: RecordReference; - declare parentIdentifier: StableRecordIdentifier; + #identifier: StableRecordIdentifier; + declare store: Store; // unsubscribe tokens given to us by the notification manager #token!: Object; @@ -42,18 +62,16 @@ export default class BelongsToReference extends Reference { @tracked _ref = 0; constructor( - store: CoreStore, + store: Store, parentIdentifier: StableRecordIdentifier, belongsToRelationship: BelongsToRelationship, key: string ) { - super(store, parentIdentifier); this.key = key; this.belongsToRelationship = belongsToRelationship; this.type = belongsToRelationship.definition.type; - const parent = internalModelFactoryFor(store).peek(parentIdentifier); - this.parent = parent!.recordReference; - this.parentIdentifier = parentIdentifier; + this.store = store; + this.#identifier = parentIdentifier; this.#token = store._notificationManager.subscribe( parentIdentifier, @@ -68,9 +86,11 @@ export default class BelongsToReference extends Reference { } destroy() { - unsubscribe(this.#token); + // TODO @feature we need the notification manager often enough + // we should potentially just expose it fully public + this.store._notificationManager.unsubscribe(this.#token); if (this.#relatedToken) { - unsubscribe(this.#relatedToken); + this.store._notificationManager.unsubscribe(this.#relatedToken); } } @@ -79,7 +99,7 @@ export default class BelongsToReference extends Reference { get _relatedIdentifier(): StableRecordIdentifier | null { this._ref; // consume the tracked prop if (this.#relatedToken) { - unsubscribe(this.#relatedToken); + this.store._notificationManager.unsubscribe(this.#relatedToken); } let resource = this._resource(); @@ -144,8 +164,170 @@ export default class BelongsToReference extends Reference { return this._relatedIdentifier?.id || null; } + /** + The link Ember Data will use to fetch or reload this belongs-to + relationship. By default it uses only the "related" resource linkage. + + Example + + ```javascript + // models/blog.js + import Model, { belongsTo } from '@ember-data/model'; + export default Model.extend({ + user: belongsTo({ async: true }) + }); + + let blog = store.push({ + data: { + type: 'blog', + id: 1, + relationships: { + user: { + links: { + related: '/articles/1/author' + } + } + } + } + }); + let userRef = blog.belongsTo('user'); + + // get the identifier of the reference + if (userRef.remoteType() === "link") { + let link = userRef.link(); + } + ``` + + @method link + @public + @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship. + */ + link(): string | null { + let resource = this._resource(); + + if (isResourceIdentiferWithRelatedLinks(resource)) { + if (resource.links) { + let related = resource.links.related; + return !related || typeof related === 'string' ? related : related.href; + } + } + return null; + } + + /** + * any links that have been received for this relationship + * + * @method links + * @public + * @returns + */ + links(): Links | null { + let resource = this._resource(); + + return resource && resource.links ? resource.links : null; + } + + /** + The meta data for the belongs-to relationship. + + Example + + ```javascript + // models/blog.js + import Model, { belongsTo } from '@ember-data/model'; + export default Model.extend({ + user: belongsTo({ async: true }) + }); + + let blog = store.push({ + data: { + type: 'blog', + id: 1, + relationships: { + user: { + links: { + related: { + href: '/articles/1/author' + }, + }, + meta: { + lastUpdated: 1458014400000 + } + } + } + } + }); + + let userRef = blog.belongsTo('user'); + + userRef.meta() // { lastUpdated: 1458014400000 } + ``` + + @method meta + @public + @return {Object} The meta information for the belongs-to relationship. + */ + meta() { + let meta: Dict | null = null; + let resource = this._resource(); + if (resource && resource.meta && typeof resource.meta === 'object') { + meta = resource.meta; + } + return meta; + } + _resource() { - return this.recordData.getBelongsTo(this.key); + return this.store._instanceCache.recordDataFor(this.#identifier, false).getBelongsTo(this.key); + } + + /** + This returns a string that represents how the reference will be + looked up when it is loaded. If the relationship has a link it will + use the "link" otherwise it defaults to "id". + + Example + + ```app/models/post.js + import Model, { hasMany } from '@ember-data/model'; + + export default class PostModel extends Model { + @hasMany({ async: true }) comments; + } + ``` + + ```javascript + let post = store.push({ + data: { + type: 'post', + id: 1, + relationships: { + comments: { + data: [{ type: 'comment', id: 1 }] + } + } + } + }); + + let commentsRef = post.hasMany('comments'); + + // get the identifier of the reference + if (commentsRef.remoteType() === "ids") { + let ids = commentsRef.ids(); + } else if (commentsRef.remoteType() === "link") { + let link = commentsRef.link(); + } + ``` + + @method remoteType + @public + @return {String} The name of the remote type. This should either be `link` or `id` + */ + remoteType(): 'link' | 'id' { + let value = this._resource(); + if (isResourceIdentiferWithRelatedLinks(value)) { + return 'link'; + } + return 'id'; } /** @@ -195,9 +377,11 @@ export default class BelongsToReference extends Reference { @return {Promise} A promise that resolves with the new value in this belongs-to relationship. */ async push(data: SingleResourceDocument | Promise): Promise { + // TODO @deprecate pushing unresolved payloads const jsonApiDoc = await resolve(data); let record = this.store.push(jsonApiDoc); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call assertPolymorphicType( this.belongsToRelationship.identifier, this.belongsToRelationship.definition, @@ -335,9 +519,11 @@ export default class BelongsToReference extends Reference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the record in this belongs-to relationship. */ - load(options) { - let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier); - return parentInternalModel!.getBelongsTo(this.key, options); + load(options?: Dict) { + const support: LegacySupport = ( + LEGACY_SUPPORT as DebugWeakCache + ).getWithError(this.#identifier); + return support.getBelongsTo(this.key, options); } /** @@ -390,10 +576,10 @@ export default class BelongsToReference extends Reference { @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) { - let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier); - return parentInternalModel!.reloadBelongsTo(this.key, options).then((internalModel) => { - return this.value(); - }); + reload(options?: Dict) { + const support: LegacySupport = ( + LEGACY_SUPPORT as DebugWeakCache + ).getWithError(this.#identifier); + return support.reloadBelongsTo(this.key, options).then(() => this.value()); } } diff --git a/packages/store/addon/-private/system/references/has-many.ts b/packages/model/addon/-private/references/has-many.ts similarity index 67% rename from packages/store/addon/-private/system/references/has-many.ts rename to packages/model/addon/-private/references/has-many.ts index d18e5662402..62833eb020b 100644 --- a/packages/store/addon/-private/system/references/has-many.ts +++ b/packages/model/addon/-private/references/has-many.ts @@ -2,28 +2,48 @@ import { dependentKeyCompat } from '@ember/object/compat'; import { DEBUG } from '@glimmer/env'; import { cached, tracked } from '@glimmer/tracking'; +import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; import { resolve } from 'rsvp'; import { ManyArray } from 'ember-data/-private'; import type { ManyRelationship } from '@ember-data/record-data/-private'; +import type Store from '@ember-data/store'; +import { recordIdentifierFor } from '@ember-data/store'; import { assertPolymorphicType } from '@ember-data/store/-debug'; - -import { +import type { NotificationType } from '@ember-data/store/-private/record-notification-manager'; +import type { DebugWeakCache } from '@ember-data/store/-private/weak-cache'; +import type { CollectionResourceDocument, + CollectionResourceRelationship, ExistingResourceObject, + LinkObject, + PaginationLinks, SingleResourceDocument, -} from '../../ts-interfaces/ember-data-json-api'; -import { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import CoreStore from '../core-store'; -import { NotificationType, unsubscribe } from '../record-notification-manager'; -import { internalModelFactoryFor, recordIdentifierFor } from '../store/internal-model-factory'; -import RecordReference from './record'; -import Reference from './reference'; +} 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 type { LegacySupport } from '../legacy-relationships-support'; +import { LEGACY_SUPPORT } from '../model'; + /** - @module @ember-data/store + @module @ember-data/model */ +interface ResourceIdentifier { + links?: { + related?: string | LinkObject; + }; + meta?: JSONObject; +} +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. @@ -32,11 +52,11 @@ import Reference from './reference'; @public @extends Reference */ -export default class HasManyReference extends Reference { +export default class HasManyReference { declare key: string; declare hasManyRelationship: ManyRelationship; declare type: string; - declare parent: RecordReference; + declare store: Store; // unsubscribe tokens given to us by the notification manager #token!: Object; @@ -46,17 +66,16 @@ export default class HasManyReference extends Reference { @tracked _ref = 0; constructor( - store: CoreStore, + store: Store, parentIdentifier: StableRecordIdentifier, hasManyRelationship: ManyRelationship, key: string ) { - super(store, parentIdentifier); this.key = key; this.hasManyRelationship = hasManyRelationship; this.type = hasManyRelationship.definition.type; - this.parent = internalModelFactoryFor(store).peek(parentIdentifier)!.recordReference; + this.store = store; this.#identifier = parentIdentifier; this.#token = store._notificationManager.subscribe( parentIdentifier, @@ -71,9 +90,9 @@ export default class HasManyReference extends Reference { } destroy() { - unsubscribe(this.#token); + this.store._notificationManager.unsubscribe(this.#token); this.#relatedTokenMap.forEach((token) => { - unsubscribe(token); + this.store._notificationManager.unsubscribe(token); }); this.#relatedTokenMap.clear(); } @@ -86,7 +105,7 @@ export default class HasManyReference extends Reference { let resource = this._resource(); this.#relatedTokenMap.forEach((token) => { - unsubscribe(token); + this.store._notificationManager.unsubscribe(token); }); this.#relatedTokenMap.clear(); @@ -112,7 +131,7 @@ export default class HasManyReference extends Reference { } _resource() { - return this.recordData.getHasMany(this.key); + return this.store._instanceCache.recordDataFor(this.#identifier, false).getHasMany(this.key); } /** @@ -205,6 +224,118 @@ export default class HasManyReference extends Reference { return this._relatedIdentifiers.map((identifier) => identifier.id); } + /** + The link Ember Data will use to fetch or reload this belongs-to + relationship. By default it uses only the "related" resource linkage. + + Example + + ```javascript + // models/blog.js + import Model, { belongsTo } from '@ember-data/model'; + export default Model.extend({ + user: belongsTo({ async: true }) + }); + + let blog = store.push({ + data: { + type: 'blog', + id: 1, + relationships: { + user: { + links: { + related: '/articles/1/author' + } + } + } + } + }); + let userRef = blog.belongsTo('user'); + + // get the identifier of the reference + if (userRef.remoteType() === "link") { + let link = userRef.link(); + } + ``` + + @method link + @public + @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship. + */ + link(): string | null { + let resource = this._resource(); + + if (isResourceIdentiferWithRelatedLinks(resource)) { + if (resource.links) { + let related = resource.links.related; + return !related || typeof related === 'string' ? related : related.href; + } + } + return null; + } + + /** + * any links that have been received for this relationship + * + * @method links + * @public + * @returns + */ + links(): PaginationLinks | null { + let resource = this._resource(); + + return resource && resource.links ? resource.links : null; + } + + /** + The meta data for the has-many relationship. + + Example + + ```javascript + // models/blog.js + import Model, { hasMany } from '@ember-data/model'; + export default Model.extend({ + users: hasMany({ async: true }) + }); + + let blog = store.push({ + data: { + type: 'blog', + id: 1, + relationships: { + users: { + links: { + related: { + href: '/articles/1/authors' + }, + }, + meta: { + lastUpdated: 1458014400000 + } + } + } + } + }); + + let usersRef = blog.hasMany('user'); + + usersRef.meta() // { lastUpdated: 1458014400000 } + ``` + + @method meta + @public + @return {Object} The meta information for the belongs-to relationship. + */ + meta() { + let meta: Dict | null = null; + let resource = this._resource(); + if (resource && resource.meta && typeof resource.meta === 'object') { + meta = resource.meta; + } + return meta; + } + /** `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 @@ -262,11 +393,10 @@ export default class HasManyReference extends Reference { array = payload as ExistingResourceObject[]; } - const internalModel = internalModelFactoryFor(this.store).peek(this.#identifier)!; const { store } = this; let identifiers = array.map((obj) => { - let record; + let record: RecordInstance; if ('data' in obj) { // TODO deprecate pushing non-valid JSON:API here record = store.push(obj); @@ -277,6 +407,8 @@ export default class HasManyReference extends Reference { if (DEBUG) { let relationshipMeta = this.hasManyRelationship.definition; let identifier = this.hasManyRelationship.identifier; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call assertPolymorphicType(identifier, relationshipMeta, recordIdentifierFor(record), store); } return recordIdentifierFor(record); @@ -292,8 +424,7 @@ export default class HasManyReference extends Reference { }); }); - // TODO IGOR it seems wrong that we were returning the many array here - return internalModel.getHasMany(this.key) as Promise | ManyArray; // this cast is necessary because typescript does not work properly with custom thenables + return this.load(); } _isLoaded() { @@ -304,9 +435,9 @@ export default class HasManyReference extends Reference { let members = this.hasManyRelationship.currentState; - //TODO Igor cleanup + //TODO @runspired determine isLoaded via a better means return members.every((identifier) => { - let internalModel = this.store._internalModelForResource(identifier); + let internalModel = this.store._instanceCache._internalModelForResource(identifier); return internalModel.isLoaded === true; }); } @@ -353,12 +484,11 @@ export default class HasManyReference extends Reference { @return {ManyArray} */ value() { - const internalModel = internalModelFactoryFor(this.store).peek(this.#identifier)!; - if (this._isLoaded()) { - return internalModel.getManyArray(this.key); - } + const support: LegacySupport = ( + LEGACY_SUPPORT as DebugWeakCache + ).getWithError(this.#identifier); - return null; + return this._isLoaded() ? support.getManyArray(this.key) : null; } /** @@ -425,9 +555,11 @@ export default class HasManyReference extends Reference { @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - load(options) { - const internalModel = internalModelFactoryFor(this.store).peek(this.#identifier)!; - return internalModel.getHasMany(this.key, options); + async load(options?: FindOptions): Promise { + const support: LegacySupport = ( + LEGACY_SUPPORT as DebugWeakCache + ).getWithError(this.#identifier); + return support.getHasMany(this.key, options) as Promise | ManyArray; // this cast is necessary because typescript does not work properly with custom thenables; } /** @@ -480,8 +612,10 @@ export default class HasManyReference extends Reference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - reload(options) { - const internalModel = internalModelFactoryFor(this.store).peek(this.#identifier)!; - return internalModel.reloadHasMany(this.key, options); + reload(options?: FindOptions) { + const support: LegacySupport = ( + LEGACY_SUPPORT as DebugWeakCache + ).getWithError(this.#identifier); + return support.reloadHasMany(this.key, options); } } diff --git a/packages/model/addon/-private/system/relationships/relationship-meta.ts b/packages/model/addon/-private/relationship-meta.ts similarity index 88% rename from packages/model/addon/-private/system/relationships/relationship-meta.ts rename to packages/model/addon/-private/relationship-meta.ts index 2349d3dbbaa..5c7ee813e5b 100644 --- a/packages/model/addon/-private/system/relationships/relationship-meta.ts +++ b/packages/model/addon/-private/relationship-meta.ts @@ -2,9 +2,9 @@ import { DEBUG } from '@glimmer/env'; import { singularize } from 'ember-inflector'; +import type Store from '@ember-data/store'; import { normalizeModelName } from '@ember-data/store/-private'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import type { RelationshipSchema } from '@ember-data/store/-private/ts-interfaces/record-data-schemas'; +import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas'; /** @module @ember-data/store @@ -67,21 +67,21 @@ export class RelationshipDefinition implements RelationshipSchema { return this.meta.name; } - _inverseKey(store: CoreStore, modelClass): string { + _inverseKey(store: Store, modelClass): string { if (this.__hasCalculatedInverse === false) { this._calculateInverse(store, modelClass); } return this.__inverseKey; } - _inverseIsAsync(store: CoreStore, modelClass): boolean { + _inverseIsAsync(store: Store, modelClass): boolean { if (this.__hasCalculatedInverse === false) { this._calculateInverse(store, modelClass); } return this.__inverseIsAsync; } - _calculateInverse(store: CoreStore, modelClass): void { + _calculateInverse(store: Store, modelClass): void { this.__hasCalculatedInverse = true; let inverseKey, inverseIsAsync; let inverse: any = null; diff --git a/packages/model/index.js b/packages/model/index.js index 8dfe1640708..f39981b5f9a 100644 --- a/packages/model/index.js +++ b/packages/model/index.js @@ -15,6 +15,9 @@ module.exports = Object.assign({}, addonBaseConfig, { '@ember-data/canary-features', '@ember-data/store', '@ember-data/store/-private', + '@ember-data/store/-debug', + '@embroider/macros', + '@embroider/macros/es-compat', '@ember/application', '@ember/array', diff --git a/packages/model/package.json b/packages/model/package.json index e2835838820..fe70edb9228 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -30,6 +30,7 @@ "ember-cli-test-info": "^1.0.0", "ember-cli-typescript": "^5.1.0", "ember-compatibility-helpers": "^1.2.6", + "@embroider/macros": "^1.8.3", "inflection": "~1.13.2" }, "devDependencies": { 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 index d9c0918773f..49b4c33ecf1 100644 --- 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 @@ -58,8 +58,8 @@ function addonBuildConfigForDataPackage(PackageName) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } else if (message.code === 'NON_EXISTENT_EXPORT') { - // ignore ts-interface imports - if (message.message.indexOf(`/ts-interfaces/`) !== -1) { + // ignore type imports + if (message.message.indexOf(`@ember-data/types`) !== -1) { return; } } else if (message.code === 'UNRESOLVED_IMPORT') { diff --git a/packages/record-data/addon/-private/graph/-edge-definition.ts b/packages/record-data/addon/-private/graph/-edge-definition.ts index 8c301c55da0..b0e61e67d2d 100644 --- a/packages/record-data/addon/-private/graph/-edge-definition.ts +++ b/packages/record-data/addon/-private/graph/-edge-definition.ts @@ -1,8 +1,8 @@ import { assert } from '@ember/debug'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RelationshipSchema } from '@ember-data/store/-private/ts-interfaces/record-data-schemas'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +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 type { Graph } from '.'; import { expandingGet, expandingSet } from './-utils'; diff --git a/packages/record-data/addon/-private/graph/-operations.ts b/packages/record-data/addon/-private/graph/-operations.ts index 4c8e8e92ae8..8a0ae6f2a91 100644 --- a/packages/record-data/addon/-private/graph/-operations.ts +++ b/packages/record-data/addon/-private/graph/-operations.ts @@ -1,8 +1,8 @@ import type { CollectionResourceRelationship, SingleResourceRelationship, -} from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +} from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; export interface Operation { op: string; diff --git a/packages/record-data/addon/-private/graph/-utils.ts b/packages/record-data/addon/-private/graph/-utils.ts index 331ff7951aa..713b7ed913a 100644 --- a/packages/record-data/addon/-private/graph/-utils.ts +++ b/packages/record-data/addon/-private/graph/-utils.ts @@ -1,14 +1,14 @@ import { assert, inspect, warn } from '@ember/debug'; import { coerceId, recordDataFor as peekRecordData } from '@ember-data/store/-private'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { RelationshipRecordData } from '@ember-data/types/q/relationship-record-data'; +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 type ImplicitRelationship from '../relationships/state/implicit'; -import type { RelationshipRecordData } from '../ts-interfaces/relationship-record-data'; import type { UpdateRelationshipOperation } from './-operations'; import type { Graph } from './index'; diff --git a/packages/record-data/addon/-private/graph/index.ts b/packages/record-data/addon/-private/graph/index.ts index adf762bf10e..66b2f8f596f 100644 --- a/packages/record-data/addon/-private/graph/index.ts +++ b/packages/record-data/addon/-private/graph/index.ts @@ -1,11 +1,11 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; +import type Store from '@ember-data/store'; import type { RecordDataStoreWrapper } from '@ember-data/store/-private'; import { WeakCache } from '@ember-data/store/-private'; -import type Store from '@ember-data/store/-private/system/core-store'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +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'; @@ -40,11 +40,11 @@ Graphs._generator = (wrapper: RecordDataStoreWrapper) => { }; function isStore(maybeStore: unknown): maybeStore is Store { - return (maybeStore as Store)._storeWrapper !== undefined; + return (maybeStore as Store)._instanceCache !== undefined; } function getWrapper(store: RecordDataStoreWrapper | Store): RecordDataStoreWrapper { - return isStore(store) ? store._storeWrapper : store; + return isStore(store) ? store._instanceCache._storeWrapper : store; } export function peekGraph(store: RecordDataStoreWrapper | Store): Graph | undefined { diff --git a/packages/record-data/addon/-private/graph/operations/add-to-related-records.ts b/packages/record-data/addon/-private/graph/operations/add-to-related-records.ts index 0c9bcfe8d87..fc630682700 100644 --- a/packages/record-data/addon/-private/graph/operations/add-to-related-records.ts +++ b/packages/record-data/addon/-private/graph/operations/add-to-related-records.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import { assertPolymorphicType } from '@ember-data/store/-debug'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type ManyRelationship from '../../relationships/state/has-many'; import type { AddToRelatedRecordsOperation } from '../-operations'; diff --git a/packages/record-data/addon/-private/graph/operations/remove-from-related-records.ts b/packages/record-data/addon/-private/graph/operations/remove-from-related-records.ts index 673b26f4b73..5d60729e587 100644 --- a/packages/record-data/addon/-private/graph/operations/remove-from-related-records.ts +++ b/packages/record-data/addon/-private/graph/operations/remove-from-related-records.ts @@ -1,6 +1,6 @@ import { assert } from '@ember/debug'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type ManyRelationship from '../../relationships/state/has-many'; import type { RemoveFromRelatedRecordsOperation } from '../-operations'; diff --git a/packages/record-data/addon/-private/graph/operations/replace-related-record.ts b/packages/record-data/addon/-private/graph/operations/replace-related-record.ts index 48e12ddc8bb..fec7235d28b 100644 --- a/packages/record-data/addon/-private/graph/operations/replace-related-record.ts +++ b/packages/record-data/addon/-private/graph/operations/replace-related-record.ts @@ -1,12 +1,12 @@ import { assert } from '@ember/debug'; import { assertPolymorphicType } from '@ember-data/store/-debug'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { ReplaceRelatedRecordOperation } from '../-operations'; import { isBelongsTo, isNew } from '../-utils'; import type { Graph } from '../index'; -import { addToInverse, removeFromInverse } from './replace-related-records'; +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); @@ -75,11 +75,15 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec if (isRemote) { const { localState } = relationship; // don't sync if localState is a new record and our canonicalState is null - if ((localState && isNew(localState) && !existingState) || localState === existingState) { + if (localState && isNew(localState) && !existingState) { return; } - relationship.localState = existingState; - relationship.notifyBelongsToChange(); + if (existingState && localState === existingState) { + notifyInverseOfPotentialMaterialization(graph, existingState, definition.inverseKey, op.record, isRemote); + } else { + relationship.localState = existingState; + relationship.notifyBelongsToChange(); + } } return; } diff --git a/packages/record-data/addon/-private/graph/operations/replace-related-records.ts b/packages/record-data/addon/-private/graph/operations/replace-related-records.ts index 347dbb361d3..d373258ae40 100644 --- a/packages/record-data/addon/-private/graph/operations/replace-related-records.ts +++ b/packages/record-data/addon/-private/graph/operations/replace-related-records.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import { assertPolymorphicType } from '@ember-data/store/-debug'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { ManyRelationship } from '../..'; import type { ReplaceRelatedRecordsOperation } from '../-operations'; @@ -292,6 +292,19 @@ export function addToInverse( } } +export function notifyInverseOfPotentialMaterialization( + graph: Graph, + identifier: StableRecordIdentifier, + key: string, + value: StableRecordIdentifier, + isRemote: boolean +) { + const relationship = graph.get(identifier, key); + if (isHasMany(relationship) && isRemote && relationship.canonicalMembers.has(value)) { + relationship.notifyHasManyChange(); + } +} + export function removeFromInverse( graph: Graph, identifier: StableRecordIdentifier, diff --git a/packages/record-data/addon/-private/graph/operations/update-relationship.ts b/packages/record-data/addon/-private/graph/operations/update-relationship.ts index acc997cab46..28f9880eedf 100644 --- a/packages/record-data/addon/-private/graph/operations/update-relationship.ts +++ b/packages/record-data/addon/-private/graph/operations/update-relationship.ts @@ -1,6 +1,6 @@ import { assert, warn } from '@ember/debug'; -import type { ExistingResourceIdentifierObject } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; +import type { ExistingResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; import _normalizeLink from '../../normalize-link'; import type { UpdateRelationshipOperation } from '../-operations'; diff --git a/packages/record-data/addon/-private/normalize-link.ts b/packages/record-data/addon/-private/normalize-link.ts index f396b77bd3f..ea18acd0e8a 100644 --- a/packages/record-data/addon/-private/normalize-link.ts +++ b/packages/record-data/addon/-private/normalize-link.ts @@ -1,4 +1,4 @@ -import type { Link, LinkObject } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; +import type { Link, LinkObject } from '@ember-data/types/q/ember-data-json-api'; /* This method normalizes a link to an "links object". If the passed link is diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts index acc942a8d74..df5d1daec31 100644 --- a/packages/record-data/addon/-private/record-data.ts +++ b/packages/record-data/addon/-private/record-data.ts @@ -7,24 +7,20 @@ import { isEqual } from '@ember/utils'; import type { RecordDataStoreWrapper } from '@ember-data/store/-private'; import { recordDataFor, recordIdentifierFor, removeRecordDataFor } from '@ember-data/store/-private'; -import type { CollectionResourceRelationship } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import type { ChangedAttributesHash, RecordData } from '@ember-data/store/-private/ts-interfaces/record-data'; +import type { CollectionResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; +import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data'; +import type { AttributesHash, JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; import type { - AttributesHash, - JsonApiResource, - JsonApiValidationError, -} from '@ember-data/store/-private/ts-interfaces/record-data-json-api'; + DefaultSingleResourceRelationship, + RelationshipRecordData, +} from '@ember-data/types/q/relationship-record-data'; import coerceId from './coerce-id'; import { isImplicit } from './graph/-utils'; import { graphFor } from './graph/index'; import type BelongsToRelationship from './relationships/state/belongs-to'; import type ManyRelationship from './relationships/state/has-many'; -import type { - DefaultSingleResourceRelationship, - RelationshipRecordData, -} from './ts-interfaces/relationship-record-data'; let nextBfsId = 1; @@ -388,7 +384,7 @@ export default class RecordDataDefault implements RelationshipRecordData { return (graphFor(this.storeWrapper).get(this.identifier, key) as BelongsToRelationship).getData(); } - setDirtyBelongsTo(key: string, recordData: RecordData) { + setDirtyBelongsTo(key: string, recordData: RecordData | null) { graphFor(this.storeWrapper).update({ op: 'replaceRelatedRecord', record: this.identifier, diff --git a/packages/record-data/addon/-private/relationships/state/belongs-to.ts b/packages/record-data/addon/-private/relationships/state/belongs-to.ts index eb70af7935f..1cc8592637c 100644 --- a/packages/record-data/addon/-private/relationships/state/belongs-to.ts +++ b/packages/record-data/addon/-private/relationships/state/belongs-to.ts @@ -1,6 +1,7 @@ import type { RecordDataStoreWrapper } from '@ember-data/store/-private'; -import type { Links, Meta, PaginationLinks } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { Links, Meta, PaginationLinks } from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { DefaultSingleResourceRelationship } from '@ember-data/types/q/relationship-record-data'; import type { ManyRelationship } from '../..'; import type { Graph } from '../../graph'; @@ -8,7 +9,6 @@ import type { UpgradedMeta } from '../../graph/-edge-definition'; import type { RelationshipState } from '../../graph/-state'; import { createState } from '../../graph/-state'; import { isNew } from '../../graph/-utils'; -import type { DefaultSingleResourceRelationship } from '../../ts-interfaces/relationship-record-data'; export default class BelongsToRelationship { declare localState: StableRecordIdentifier | null; diff --git a/packages/record-data/addon/-private/relationships/state/has-many.ts b/packages/record-data/addon/-private/relationships/state/has-many.ts index 57f51cf8d33..fabbed87401 100755 --- a/packages/record-data/addon/-private/relationships/state/has-many.ts +++ b/packages/record-data/addon/-private/relationships/state/has-many.ts @@ -6,8 +6,8 @@ import type { Links, Meta, PaginationLinks, -} from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +} from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { BelongsToRelationship } from '../..'; import type { Graph } from '../../graph'; diff --git a/packages/record-data/addon/-private/relationships/state/implicit.ts b/packages/record-data/addon/-private/relationships/state/implicit.ts index 4b961c26757..5f4a4dc4320 100644 --- a/packages/record-data/addon/-private/relationships/state/implicit.ts +++ b/packages/record-data/addon/-private/relationships/state/implicit.ts @@ -1,4 +1,4 @@ -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { Graph } from '../../graph'; import type { UpgradedMeta } from '../../graph/-edge-definition'; diff --git a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts index 2801f5af51f..7e2d656bd69 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/helpers.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/helpers.ts @@ -3,7 +3,7 @@ import { settled } from '@ember/test-helpers'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import type { Relationship as ImplicitRelationship } from '@ember-data/record-data/-private'; import { recordIdentifierFor } from '@ember-data/store'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { Context, UserRecord } from './setup'; import { stateOf } from './setup'; diff --git a/packages/record-data/tests/integration/graph/edge-removal/setup.ts b/packages/record-data/tests/integration/graph/edge-removal/setup.ts index 7a5c7f4954a..718355faa21 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/setup.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/setup.ts @@ -7,23 +7,22 @@ import type { } from '@ember-data/record-data/-private'; import { graphFor } from '@ember-data/record-data/-private'; import Store from '@ember-data/store'; -import type CoreStore from '@ember-data/store/-private/system/core-store'; -import { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; +import type { DSModel } from '@ember-data/types/q/ds-model'; import type { CollectionResourceDocument, EmptyResourceDocument, JsonApiDocument, SingleResourceDocument, -} from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; -import { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +} 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: CoreStore, private isImplicit: boolean) {} + constructor(private store: Store, private isImplicit: boolean) {} has(identifier: StableRecordIdentifier) { - let graph = graphFor(this.store._storeWrapper); + let graph = graphFor(this.store); return graph.identifiers.has(identifier); } } @@ -32,7 +31,7 @@ class AbstractGraph { public identifiers: AbstractMap; public implicit: { has(identifier: StableRecordIdentifier): boolean }; - constructor(private store: CoreStore) { + constructor(private store: Store) { this.identifiers = new AbstractMap(store, false); this.implicit = { has: (identifier) => { @@ -45,11 +44,11 @@ class AbstractGraph { identifier: StableRecordIdentifier, propertyName: string ): ManyRelationship | BelongsToRelationship | ImplicitRelationship { - return graphFor(this.store._storeWrapper).get(identifier, propertyName); + return graphFor(this.store).get(identifier, propertyName); } getImplicit(identifier: StableRecordIdentifier): Dict { - const rels = graphFor(this.store._storeWrapper).identifiers.get(identifier); + const rels = graphFor(this.store).identifiers.get(identifier); let implicits = Object.create(null); if (rels) { Object.keys(rels).forEach((key) => { @@ -63,7 +62,7 @@ class AbstractGraph { } } -function graphForTest(store: CoreStore) { +function graphForTest(store: Store) { return new AbstractGraph(store); } @@ -145,7 +144,7 @@ export interface Context { owner: any; } -interface TestStore extends CoreStore { +interface TestStore extends Store { push(data: EmptyResourceDocument): null; push(data: SingleResourceDocument): T; push(data: CollectionResourceDocument): T[]; diff --git a/packages/record-data/tests/integration/graph/graph-test.ts b/packages/record-data/tests/integration/graph/graph-test.ts index 52b44c5dcb5..16b80cb0f65 100644 --- a/packages/record-data/tests/integration/graph/graph-test.ts +++ b/packages/record-data/tests/integration/graph/graph-test.ts @@ -21,7 +21,7 @@ module('Integration | Graph | Configuration', function (hooks) { }); test('graphFor util returns the same graph instance for repeated calls on the same store wrapper instance', async function (assert) { - const wrapper = store._storeWrapper; + const wrapper = store._instanceCache._storeWrapper; const graph1 = graphFor(wrapper); const graph2 = graphFor(wrapper); const graph3 = graphFor(wrapper); @@ -32,15 +32,15 @@ module('Integration | Graph | Configuration', function (hooks) { test('graphFor util returns a new graph instance for each unique store wrapper', async function (assert) { const { owner } = this; - const wrapper1 = store._storeWrapper; + 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._storeWrapper; - const wrapper3 = store3._storeWrapper; + const wrapper2 = store2._instanceCache._storeWrapper; + const wrapper3 = store3._instanceCache._storeWrapper; const graph1 = graphFor(wrapper1); const graph2 = graphFor(wrapper2); @@ -79,14 +79,14 @@ module('Integration | Graph | Configuration', function (hooks) { test('graphFor util returns the same graph instance for the store and storeWrapper', async function (assert) { const { owner } = this; - const wrapper = store._storeWrapper; + 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._storeWrapper; + const wrapper2 = store2._instanceCache._storeWrapper; // lookup the store first const graph3 = graphFor(store2); const graph4 = graphFor(wrapper2); diff --git a/packages/record-data/tests/integration/graph/operations-test.ts b/packages/record-data/tests/integration/graph/operations-test.ts index a0d9e210732..6b63f09dc98 100644 --- a/packages/record-data/tests/integration/graph/operations-test.ts +++ b/packages/record-data/tests/integration/graph/operations-test.ts @@ -53,10 +53,10 @@ module('Integration | Graph | Operations', function (hooks) { JSON.parse(JSON.stringify(data.getData())), { data: [ - { type: 'config', id: '1', lid: '@ember-data:lid-config-1' }, - { type: 'config', id: '2', lid: '@ember-data:lid-config-2' }, - { type: 'config', id: '3', lid: '@ember-data:lid-config-3' }, - { type: 'config', id: '4', lid: '@ember-data:lid-config-4' }, + { 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' @@ -107,10 +107,10 @@ module('Integration | Graph | Operations', function (hooks) { JSON.parse(JSON.stringify(data.getData())), { data: [ - { type: 'config', id: '1', lid: '@ember-data:lid-config-1' }, - { type: 'config', id: '2', lid: '@ember-data:lid-config-2' }, - { type: 'config', id: '3', lid: '@ember-data:lid-config-3' }, - { type: 'config', id: '4', lid: '@ember-data:lid-config-4' }, + { 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/packages/record-data/types/@ember/polyfills/index.d.ts b/packages/record-data/types/@ember/polyfills/index.d.ts deleted file mode 100644 index cac10f55811..00000000000 --- a/packages/record-data/types/@ember/polyfills/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Copy properties from a source object to a target object. - * https://github.com/DefinitelyTyped/DefinitelyTyped/issues/38681 - */ -export const assign = Object.assign; diff --git a/packages/store/addon/-private/system/backburner.js b/packages/store/addon/-private/backburner.js similarity index 100% rename from packages/store/addon/-private/system/backburner.js rename to packages/store/addon/-private/backburner.js diff --git a/packages/store/addon/-private/system/coerce-id.ts b/packages/store/addon/-private/coerce-id.ts similarity index 100% rename from packages/store/addon/-private/system/coerce-id.ts rename to packages/store/addon/-private/coerce-id.ts diff --git a/packages/store/addon/-private/system/store/common.js b/packages/store/addon/-private/common.js similarity index 100% rename from packages/store/addon/-private/system/store/common.js rename to packages/store/addon/-private/common.js diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/core-store.ts similarity index 74% rename from packages/store/addon/-private/system/core-store.ts rename to packages/store/addon/-private/core-store.ts index ab392b41e28..73263af5a99 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/core-store.ts @@ -2,106 +2,78 @@ @module @ember-data/store */ import { getOwner, setOwner } from '@ember/application'; -import { assert, deprecate, inspect, warn } from '@ember/debug'; +import { assert, deprecate } from '@ember/debug'; import { _backburner as emberBackburner } from '@ember/runloop'; import type { Backburner } from '@ember/runloop/-private/backburner'; import Service from '@ember/service'; import { registerWaiter, unregisterWaiter } from '@ember/test'; import { DEBUG } from '@glimmer/env'; -import Ember from 'ember'; import { importSync } from '@embroider/macros'; -import { all, reject, resolve } from 'rsvp'; +import { reject, resolve } from 'rsvp'; import type DSModelClass from '@ember-data/model'; -import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; +import { HAS_MODEL_PACKAGE, HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_HAS_RECORD, DEPRECATE_JSON_API_FALLBACK, DEPRECATE_RECORD_WAS_INVALID, DEPRECATE_STORE_FIND, } from '@ember-data/private-build-infra/deprecations'; -import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-data/record-data/-private'; -import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; - -import { IdentifierCache } from '../identifiers/cache'; -import { InstanceCache } from '../instance-cache'; -import type { DSModel } from '../ts-interfaces/ds-model'; +import type { RecordData as RecordDataClass } from '@ember-data/record-data/-private'; +import type { DSModel } from '@ember-data/types/q/ds-model'; import type { CollectionResourceDocument, EmptyResourceDocument, - ExistingResourceObject, JsonApiDocument, ResourceIdentifierObject, SingleResourceDocument, -} from '../ts-interfaces/ember-data-json-api'; -import type { - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '../ts-interfaces/identifier'; -import { MinimumAdapterInterface } from '../ts-interfaces/minimum-adapter-interface'; -import type { MinimumSerializerInterface } from '../ts-interfaces/minimum-serializer-interface'; -import type { RecordData } from '../ts-interfaces/record-data'; -import type { JsonApiRelationship } from '../ts-interfaces/record-data-json-api'; -import type { RecordDataRecordWrapper } from '../ts-interfaces/record-data-record-wrapper'; -import type { AttributesSchema, RelationshipsSchema } from '../ts-interfaces/record-data-schemas'; -import type { RecordInstance } from '../ts-interfaces/record-instance'; -import type { SchemaDefinitionService } from '../ts-interfaces/schema-definition-service'; -import type { FindOptions } from '../ts-interfaces/store'; -import type { Dict } from '../ts-interfaces/utils'; -import constructResource from '../utils/construct-resource'; -import promiseRecord from '../utils/promise-record'; +} 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 { RecordData } from '@ember-data/types/q/record-data'; +import type { RecordDataRecordWrapper } from '@ember-data/types/q/record-data-record-wrapper'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service'; +import type { FindOptions } from '@ember-data/types/q/store'; +import type { Dict } from '@ember-data/types/q/utils'; + import edBackburner from './backburner'; import coerceId, { ensureStringId } from './coerce-id'; import FetchManager, { SaveOp } from './fetch-manager'; -import type InternalModel from './model/internal-model'; +import { _findAll, _query, _queryRecord } from './finders'; +import { IdentifierCache } from './identifier-cache'; +import { InstanceCache, storeFor, StoreMap } from './instance-cache'; import { - assertRecordsPassedToHasMany, - extractRecordDataFromRecord, - extractRecordDatasFromRecords, -} from './model/internal-model'; + internalModelFactoryFor, + peekRecordIdentifier, + recordIdentifierFor, + setRecordIdentifier, +} from './internal-model-factory'; +import RecordReference from './model/record-reference'; import type ShimModelClass from './model/shim-model-class'; import { getShimClass } from './model/shim-model-class'; import normalizeModelName from './normalize-model-name'; import { PromiseArray, promiseArray, PromiseObject, promiseObject } from './promise-proxies'; import RecordArrayManager from './record-array-manager'; -import { AdapterPopulatedRecordArray, RecordArray } from './record-arrays'; +import AdapterPopulatedRecordArray from './record-arrays/adapter-populated-record-array'; +import RecordArray from './record-arrays/record-array'; import { setRecordDataFor } from './record-data-for'; +import RecordDataStoreWrapper from './record-data-store-wrapper'; import NotificationManager from './record-notification-manager'; -import type { BelongsToReference, HasManyReference } from './references'; -import { RecordReference } from './references'; import type RequestCache from './request-cache'; import { DSModelSchemaDefinitionService, getModelFactory } from './schema-definition-service'; -import { _findAll, _findBelongsTo, _findHasMany, _query, _queryRecord } from './store/finders'; -import { - internalModelFactoryFor, - peekRecordIdentifier, - recordIdentifierFor, - setRecordIdentifier, -} from './store/internal-model-factory'; -import RecordDataStoreWrapper from './store/record-data-store-wrapper'; -import WeakCache from './weak-cache'; +import constructResource from './utils/construct-resource'; +import promiseRecord from './utils/promise-record'; + +export { storeFor }; type RecordDataConstruct = typeof RecordDataClass; let _RecordData: RecordDataConstruct | undefined; -const { ENV } = Ember; type AsyncTrackingToken = Readonly<{ label: string; trace: Error | string }>; -const RECORD_REFERENCES = new WeakCache(DEBUG ? 'reference' : ''); -const StoreMap = new WeakCache(DEBUG ? 'store' : ''); - -export function storeFor(record: RecordInstance): CoreStore | undefined { - const store = StoreMap.get(record); - - assert( - `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`, - store - ); - return store; -} - function freeze(obj: T): T { if (typeof Object.freeze === 'function') { return Object.freeze(obj); @@ -189,7 +161,7 @@ export interface CreateRecordProperties { @extends Ember.Service */ -class CoreStore extends Service { +class Store extends Service { /** * Ember Data uses several specialized micro-queues for organizing and coalescing similar async work. @@ -206,17 +178,13 @@ class CoreStore extends Service { declare _notificationManager: NotificationManager; declare identifierCache: IdentifierCache; - declare _adapterCache: Dict; - declare _serializerCache: Dict; - declare _storeWrapper: RecordDataStoreWrapper; + declare _adapterCache: Dict; + declare _serializerCache: Dict; + declare _modelFactoryCache: Dict; declare _fetchManager: FetchManager; declare _schemaDefinitionService: SchemaDefinitionService; declare _instanceCache: InstanceCache; - declare _modelFactoryCache; - declare _relationshipsDefCache; - declare _attributesDefCache; - // DEBUG-only properties declare _trackedAsyncRequests: AsyncTrackingToken[]; declare generateStackTracesForTrackedRequests: boolean; @@ -230,24 +198,6 @@ class CoreStore extends Service { */ constructor() { super(...arguments); - this._adapterCache = Object.create(null); - this._serializerCache = Object.create(null); - this._storeWrapper = new RecordDataStoreWrapper(this); - this._backburner = edBackburner; - this.recordArrayManager = new RecordArrayManager({ store: this }); - this._instanceCache = new InstanceCache(this); - - this._modelFactoryCache = Object.create(null); - this._relationshipsDefCache = Object.create(null); - this._attributesDefCache = Object.create(null); - - RECORD_REFERENCES._generator = (identifier) => { - return new RecordReference(this, identifier); - }; - - this._fetchManager = new FetchManager(this); - this._notificationManager = new NotificationManager(this); - this.__recordDataFor = this.__recordDataFor.bind(this); /** * Provides access to the IdentifierCache instance @@ -261,6 +211,23 @@ class CoreStore extends Service { */ this.identifierCache = new IdentifierCache(); + // private but maybe useful to be here, somewhat intimate + this.recordArrayManager = new RecordArrayManager({ store: this }); + + // private, TODO consider taking public as the instance is public to instantiateRecord anyway + this._notificationManager = new NotificationManager(this); + + // private + this._fetchManager = new FetchManager(this); + this._instanceCache = new InstanceCache(this); + this._adapterCache = Object.create(null); + this._serializerCache = Object.create(null); + this._modelFactoryCache = Object.create(null); + + // private + // TODO we should find a path to something simpler than backburner + this._backburner = edBackburner; + if (DEBUG) { if (this.generateStackTracesForTrackedRequests === undefined) { this.generateStackTracesForTrackedRequests = false; @@ -312,127 +279,66 @@ class CoreStore extends Service { return this._fetchManager.requestCache; } - // TODO move this to InstanceCache - _instantiateRecord( - recordData: RecordData, - identifier: StableRecordIdentifier, - properties?: { [key: string]: unknown } - ) { - // assert here - if (properties !== undefined) { - assert( - `You passed '${properties}' as properties for record creation instead of an object.`, - typeof properties === 'object' && properties !== null - ); - - const { type } = identifier; - - // convert relationship Records to RecordDatas before passing to RecordData - let defs = this._relationshipsDefinitionFor({ type }); - - if (defs !== null) { - let keys = Object.keys(properties); - let relationshipValue; - - for (let i = 0; i < keys.length; i++) { - let prop = keys[i]; - let def = defs[prop]; - - if (def !== undefined) { - if (def.kind === 'hasMany') { - if (DEBUG) { - assertRecordsPassedToHasMany(properties[prop]); - } - relationshipValue = extractRecordDatasFromRecords(properties[prop]); - } else { - relationshipValue = extractRecordDataFromRecord(properties[prop]); - } - - properties[prop] = relationshipValue; - } - } - } - } - - // TODO guard against initRecordOptions no being there - let createOptions = recordData._initRecordCreateOptions(properties); - //TODO Igor pass a wrapper instead of RD - let record = this.instantiateRecord(identifier, createOptions, this.__recordDataFor, this._notificationManager); - setRecordIdentifier(record, identifier); - setRecordDataFor(record, recordData); - StoreMap.set(record, this); - return record; - } - instantiateRecord( identifier: StableRecordIdentifier, createRecordArgs: { [key: string]: unknown }, recordDataFor: (identifier: StableRecordIdentifier) => RecordDataRecordWrapper, notificationManager: NotificationManager ): DSModel | RecordInstance { - let modelName = identifier.type; - let store = this; - - let internalModel = this._internalModelForResource(identifier); - let createOptions: any = { - _internalModel: internalModel, - // TODO deprecate allowing unknown args setting - _createProps: createRecordArgs, - // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for - _secretInit: (record: RecordInstance): void => { - setRecordIdentifier(record, identifier); - StoreMap.set(record, store); - setRecordDataFor(record, internalModel._recordData); - }, - container: null, // necessary hack for setOwner? - }; - - // ensure that `getOwner(this)` works inside a model instance - setOwner(createOptions, getOwner(this)); - delete createOptions.container; - - let record = this._modelFactoryFor(modelName).create(createOptions); + if (HAS_MODEL_PACKAGE) { + let modelName = identifier.type; + let store = this; + + let internalModel = this._instanceCache._internalModelForResource(identifier); + let createOptions: any = { + _internalModel: internalModel, + // TODO deprecate allowing unknown args setting + _createProps: createRecordArgs, + // TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for + _secretInit: (record: RecordInstance): void => { + setRecordIdentifier(record, identifier); + StoreMap.set(record, store); + setRecordDataFor(record, internalModel._recordData); + }, + container: null, // necessary hack for setOwner? + }; - return record; + // ensure that `getOwner(this)` works inside a model instance + setOwner(createOptions, getOwner(this)); + delete createOptions.container; + // TODO this needs to not use the private property here to get modelFactoryCache so as to not break interop + return getModelFactory(this, this._modelFactoryCache, modelName).create(createOptions); + } + assert(`You must implement the store's instantiateRecord hook for your custom model class.`); } - // TODO move this to InstanceCache - _teardownRecord(record: DSModel | RecordInstance) { - StoreMap.delete(record); - // TODO remove identifier:record cache link - this.teardownRecord(record); - } teardownRecord(record: DSModel | RecordInstance): void { - 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(); + 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`); + } } getSchemaDefinitionService(): SchemaDefinitionService { - if (!this._schemaDefinitionService) { + if (HAS_MODEL_PACKAGE && !this._schemaDefinitionService) { this._schemaDefinitionService = new DSModelSchemaDefinitionService(this); } + assert( + `You must registerSchemaDefinitionService with the store to use custom model classes`, + this._schemaDefinitionService + ); return this._schemaDefinitionService; } - _attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - return this.getSchemaDefinitionService().attributesDefinitionFor(identifier); - } - - _relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - return this.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - } - registerSchemaDefinitionService(schema: SchemaDefinitionService) { this._schemaDefinitionService = schema; } - _relationshipMetaFor(modelName: string, id: string | null, key: string) { - return this._relationshipsDefinitionFor({ type: modelName })[key]; - } - /** Returns the schema for a particular `modelName`. @@ -449,6 +355,7 @@ class CoreStore extends Service { @param {String} modelName @return {subclass of Model | ShimModelClass} */ + // TODO @deprecate in favor of schema APIs, requires adapter/serializer overhaul or replacement modelFor(modelName: string): ShimModelClass | DSModelClass { if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); @@ -458,42 +365,36 @@ class CoreStore extends Service { `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); + // TODO this is safe only because + // apps would be horribly broken if the schema service were using DS_MODEL but not using DS_MODEL's schema service. + // 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. + let maybeFactory = getModelFactory(this, this._modelFactoryCache, normalizedModelName); + + // for factorFor factory/class split + let klass = maybeFactory && maybeFactory.class ? maybeFactory.class : maybeFactory; + if (!klass || !klass.isModel) { + assert( + `No model was found for '${modelName}' and no schema handles the type`, + this.getSchemaDefinitionService().doesTypeExist(modelName) + ); - let maybeFactory = this._modelFactoryFor(modelName); - - // for factorFor factory/class split - let klass = maybeFactory && maybeFactory.class ? maybeFactory.class : maybeFactory; - if (!klass || !klass.isModel) { - assert( - `No model was found for '${modelName}' and no schema handles the type`, - this.getSchemaDefinitionService().doesTypeExist(modelName) - ); - - return getShimClass(this, modelName); - } else { - return klass; + return getShimClass(this, modelName); + } else { + // TODO @deprecate ever returning the klass, always return the shim + return klass; + } } - } - _modelFactoryFor(modelName: string): DSModelClass { - if (DEBUG) { - assertDestroyedStoreOnly(this, '_modelFactoryFor'); - } - assert(`You need to pass a model name to the store's _modelFactoryFor method`, modelName); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `No model was found for '${modelName}' and no schema handles the type`, + this.getSchemaDefinitionService().doesTypeExist(modelName) ); - let normalizedModelName = normalizeModelName(modelName); - let factory = getModelFactory(this, this._modelFactoryCache, normalizedModelName); - - return factory; + return getShimClass(this, modelName); } - // ..................... - // . CREATE NEW RECORD . - // ..................... - /** Create a new record in the current store. The properties passed to this method are set on the newly created record. @@ -573,10 +474,6 @@ class CoreStore extends Service { }); } - // ................. - // . DELETE RECORD . - // ................. - /** For symmetry, a record can be deleted via the store. @@ -638,10 +535,6 @@ class CoreStore extends Service { } } - // ................ - // . FIND RECORDS . - // ................ - /** @method find @param {String} modelName @@ -1097,7 +990,7 @@ class CoreStore extends Service { // if not loaded start loading if (!internalModel.isLoaded) { - promise = this._fetchDataIfNeededForIdentifier(identifier, options); + promise = this._instanceCache._fetchDataIfNeededForIdentifier(identifier, options); // Refetch if the reload option is passed } else if (options.reload) { @@ -1135,54 +1028,6 @@ class CoreStore extends Service { return promiseRecord(this, promise, `DS: Store#findRecord ${identifier}`); } - _fetchDataIfNeededForIdentifier( - identifier: StableRecordIdentifier, - options: FindOptions = {} - ): Promise { - const cache = this._instanceCache; - const internalModel = cache.getInternalModel(identifier); - - // pre-loading will change the isEmpty value - // TODO stpre this state somewhere other than InternalModel - const { isEmpty, isLoading } = internalModel; - - if (options.preload) { - this._backburner.join(() => { - internalModel.preloadData(options.preload); - }); - } - - let promise; - if (isEmpty) { - assertIdentifierHasId(identifier); - - promise = this._fetchManager.scheduleFetch(identifier, options); - } else if (isLoading) { - promise = this._fetchManager.getPendingFetch(identifier, options); - assert(`Expected to find a pending request for a record in the loading state, but found none`, promise); - } else { - promise = resolve(identifier); - } - - return promise; - } - - _scheduleFetchMany( - identifiers: StableRecordIdentifier[], - options: FindOptions = {} - ): Promise { - let fetches = new Array(identifiers.length); - const manager = this._fetchManager; - - for (let i = 0; i < identifiers.length; i++) { - let identifier = identifiers[i]; - assertIdentifierHasId(identifier); - fetches[i] = manager.scheduleFetch(identifier, options); - } - - return all(fetches); - } - /** Get the reference for the specified record. @@ -1221,6 +1066,7 @@ class CoreStore extends Service { @since 2.5.0 @return {RecordReference} */ + // TODO @deprecate getReference (and references generally) getReference(resource: string | ResourceIdentifierObject, id: string | number): RecordReference { if (DEBUG) { assertDestroyingStore(this, 'getReference'); @@ -1241,9 +1087,8 @@ class CoreStore extends Service { ); let identifier: StableRecordIdentifier = this.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier); - if (identifier) { - return RECORD_REFERENCES.lookup(identifier); - } + + return this._instanceCache.getReference(identifier); } /** @@ -1373,167 +1218,6 @@ class CoreStore extends Service { assert(`store.hasRecordForId has been removed`); } - _findHasManyByJsonApiResource( - resource, - parentIdentifier: StableRecordIdentifier, - relationship: ManyRelationship, - options?: FindOptions - ): Promise { - if (HAS_RECORD_DATA_PACKAGE) { - if (!resource) { - return resolve(); - } - const { definition, state } = relationship; - let adapter = this.adapterFor(definition.type); - - let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state; - const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this, resource); - - let shouldFindViaLink = - resource.links && - resource.links.related && - (typeof adapter.findHasMany === 'function' || typeof resource.data === 'undefined') && - (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - - // fetch via link - if (shouldFindViaLink) { - // findHasMany, although not public, does not need to care about our upgrade relationship definitions - // and can stick with the public definition API for now. - const relationshipMeta = this._storeWrapper.relationshipsDefinitionFor(definition.inverseType)[definition.key]; - let adapter = this.adapterFor(parentIdentifier.type); - - /* - If a relationship was originally populated by the adapter as a link - (as opposed to a list of IDs), this method is called when the - relationship is fetched. - - The link (which is usually a URL) is passed through unchanged, so the - adapter can make whatever request it wants. - - The usual use-case is for the server to register a URL as a link, and - then use that URL in the future to make a request for the relationship. - */ - assert( - `You tried to load a hasMany relationship but you have no adapter (for ${parentIdentifier.type})`, - adapter - ); - assert( - `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' - ); - - return _findHasMany(adapter, this, parentIdentifier, resource.links.related, relationshipMeta, options); - } - - let preferLocalCache = hasReceivedData && !isEmpty; - - let hasLocalPartialData = - hasDematerializedInverse || (isEmpty && Array.isArray(resource.data) && resource.data.length > 0); - - // fetch using data, pulling from local cache if possible - if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { - let finds = new Array(resource.data.length); - for (let i = 0; i < resource.data.length; i++) { - let identifier = this.identifierCache.getOrCreateRecordIdentifier(resource.data[i]); - finds[i] = this._fetchDataIfNeededForIdentifier(identifier, options); - } - - return all(finds); - } - - let hasData = hasReceivedData && !isEmpty; - - // fetch by data - if (hasData || hasLocalPartialData) { - let identifiers = resource.data.map((json) => this.identifierCache.getOrCreateRecordIdentifier(json)); - - return this._scheduleFetchMany(identifiers, options); - } - - // we were explicitly told we have no data and no links. - // TODO if the relationshipIsStale, should we hit the adapter anyway? - return resolve(); - } - assert(`hasMany only works with the @ember-data/record-data package`); - } - - _findBelongsToByJsonApiResource( - resource, - parentIdentifier: StableRecordIdentifier, - relationshipMeta, - options: FindOptions = {} - ): Promise { - if (DEBUG) { - assertDestroyingStore(this, '_findBelongsToByJsonApiResource'); - } - if (!resource) { - return resolve(null); - } - - const internalModel = resource.data ? this._internalModelForResource(resource.data) : null; - - let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship - .state as RelationshipState; - const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this, resource); - - let shouldFindViaLink = - resource.links && - resource.links.related && - (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - - if (internalModel) { - // short circuit if we are already loading - let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options); - if (pendingRequest) { - return pendingRequest; - } - } - - // fetch via link - if (shouldFindViaLink) { - return _findBelongsTo(this, parentIdentifier, resource.links.related, relationshipMeta, options); - } - - let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty; - let hasLocalPartialData = hasDematerializedInverse || (isEmpty && resource.data); - // null is explicit empty, undefined is "we don't know anything" - let localDataIsEmpty = resource.data === undefined || resource.data === null; - - // fetch using data, pulling from local cache if possible - if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) { - /* - We have canonical data, but our local state is empty - */ - if (localDataIsEmpty) { - return resolve(null); - } - - if (!internalModel) { - assert(`No InternalModel found for ${resource.lid}`, internalModel); - } - - return this._fetchDataIfNeededForIdentifier(internalModel.identifier, options); - } - - let resourceIsLocal = !localDataIsEmpty && resource.data.id === null; - - if (internalModel && resourceIsLocal) { - return resolve(internalModel.identifier); - } - - // fetch by data - if (internalModel && !localDataIsEmpty) { - let identifier = internalModel.identifier; - assertIdentifierHasId(identifier); - - return this._fetchManager.scheduleFetch(identifier, options); - } - - // we were explicitly told we have no data and no links. - // TODO if the relationshipIsStale, should we hit the adapter anyway? - return resolve(null); - } - /** This method delegates a query to the adapter. This is the one place where adapter-level semantics are exposed to the application. @@ -1601,20 +1285,10 @@ class CoreStore extends Service { if (options && options.adapterOptions) { adapterOptionsWrapper.adapterOptions = options.adapterOptions; } + let recordArray = options?._recordArray || null; let normalizedModelName = normalizeModelName(modelName); - return promiseArray(this._query(normalizedModelName, query, null, adapterOptionsWrapper)); - } - - _query(modelName: string, query, array, options): Promise { - assert(`You need to pass a model name to the store's query method`, modelName); - 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' - ); - - let adapter = this.adapterFor(modelName); + let adapter = this.adapterFor(normalizedModelName); assert(`You tried to load a query but you have no adapter (for ${modelName})`, adapter); assert( @@ -1622,7 +1296,16 @@ class CoreStore extends Service { typeof adapter.query === 'function' ); - return _query(adapter, this, modelName, query, array, options) as unknown as Promise; + let queryPromise = _query( + adapter, + this, + normalizedModelName, + query, + recordArray, + adapterOptionsWrapper + ) as unknown as Promise; + + return promiseArray(queryPromise); } /** @@ -2079,10 +1762,6 @@ class CoreStore extends Service { } } - // .............. - // . PERSISTING . - // .............. - /** This method is called once the promise returned by an adapter's `createRecord`, `updateRecord` or `deleteRecord` @@ -2114,115 +1793,6 @@ class CoreStore extends Service { assert(`store.recordWasInvalid has been removed`); } - /** - Sets newly received ID from the adapter's `createRecord`, `updateRecord` - or `deleteRecord`. - - @method setRecordId - @private - @param {String} modelName - @param {string} newId - @param {string} clientId - */ - // TODO move this into one of the caches - setRecordId(modelName: string, newId: string, clientId: string) { - if (DEBUG) { - assertDestroyingStore(this, 'setRecordId'); - } - internalModelFactoryFor(this).setRecordId(modelName, newId, clientId); - } - - // ................ - // . LOADING DATA . - // ................ - - /** - This internal method is used by `push`. - - @method _load - @private - @param {Object} data - */ - _load(data: ExistingResourceObject): StableExistingRecordIdentifier { - // TODO type should be pulled from the identifier for debug - 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.getSchemaDefinitionService().doesTypeExist(modelName) - ); - - if (DEBUG) { - // If ENV.DS_WARN_ON_UNKNOWN_KEYS is set to true and the payload - // contains unknown attributes or relationships, log a warning. - - // TODO @runspired @deprecate in favor of a build-time config not in ENV - if (ENV.DS_WARN_ON_UNKNOWN_KEYS) { - let unknownAttributes, unknownRelationships; - let relationships = this.getSchemaDefinitionService().relationshipsDefinitionFor({ type: modelName }); - let attributes = this.getSchemaDefinitionService().attributesDefinitionFor({ type: modelName }); - // Check unknown attributes - unknownAttributes = Object.keys(data.attributes || {}).filter((key) => { - return !attributes[key]; - }); - - // Check unknown relationships - unknownRelationships = Object.keys(data.relationships || {}).filter((key) => { - return !relationships[key]; - }); - let unknownAttributesMessage = `The payload for '${modelName}' contains these unknown attributes: ${unknownAttributes}. Make sure they've been defined in your model.`; - warn(unknownAttributesMessage, unknownAttributes.length === 0, { - id: 'ds.store.unknown-keys-in-payload', - }); - - let unknownRelationshipsMessage = `The payload for '${modelName}' contains these unknown relationships: ${unknownRelationships}. Make sure they've been defined in your model.`; - warn(unknownRelationshipsMessage, unknownRelationships.length === 0, { - id: 'ds.store.unknown-keys-in-payload', - }); - } - } - - // TODO this should determine identifier via the cache before making assumptions - const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid)); - const maybeIdentifier = this.identifierCache.peekRecordIdentifier(resource); - - let internalModel = internalModelFactoryFor(this).lookup(resource, data); - - // 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 - const isLoading = internalModel.isLoading || (!internalModel.isLoaded && maybeIdentifier); - const isUpdate = internalModel.isEmpty === false && !isLoading; - - // exclude store.push (root.empty) case - let identifier = internalModel.identifier; - if (isUpdate || isLoading) { - let updatedIdentifier = this.identifierCache.updateRecordIdentifier(identifier, data); - - if (updatedIdentifier !== identifier) { - // we encountered a merge of identifiers in which - // two identifiers (and likely two internalModels) - // existed for the same resource. Now that we have - // determined the correct identifier to use, make sure - // that we also use the correct internalModel. - identifier = updatedIdentifier; - internalModel = internalModelFactoryFor(this).lookup(identifier); - } - } - - internalModel.setupData(data); - - if (!isUpdate) { - this.recordArrayManager.recordDidChange(identifier); - } - - return identifier as StableExistingRecordIdentifier; - } - /** Push some data for a given type into the store. @@ -2414,7 +1984,7 @@ class CoreStore extends Service { if (included) { for (i = 0, length = included.length; i < length; i++) { - this._load(included[i]); + this._instanceCache._load(included[i]); } } @@ -2423,7 +1993,7 @@ class CoreStore extends Service { let identifiers = new Array(length); for (i = 0; i < length; i++) { - identifiers[i] = this._load(jsonApiDoc.data[i]); + identifiers[i] = this._instanceCache._load(jsonApiDoc.data[i]); } return identifiers; } @@ -2439,7 +2009,7 @@ class CoreStore extends Service { typeof jsonApiDoc.data === 'object' ); - return this._load(jsonApiDoc.data); + return this._instanceCache._load(jsonApiDoc.data); }); // this typecast is necessary because `backburner.join` is mistyped to return void @@ -2532,18 +2102,13 @@ class CoreStore extends Service { serializer.pushPayload(this, payload); } - // TODO string candidate for early elimination - _internalModelForResource(resource: ResourceIdentifierObject): InternalModel { - return internalModelFactoryFor(this).getByResource(resource); - } - // 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 return this._instanceCache.createSnapshot(recordIdentifierFor(record)).serialize(options); } - // todo @runspired this should likely be publicly documented for custom records + // todo @runspired this should likely be publicly @documented for custom records saveRecord(record: RecordInstance, options: Dict = {}): Promise { assert(`Unable to initate save for a record in a disconnected state`, storeFor(record)); let identifier = recordIdentifierFor(record); @@ -2636,32 +2201,6 @@ class CoreStore extends Service { ); } - // TODO move this to InstanceCache - // TODO this is probably a public API from custom model classes? If not move to InstanceCache - relationshipReferenceFor(identifier: RecordIdentifier, key: string): BelongsToReference | HasManyReference { - let stableIdentifier = this.identifierCache.getOrCreateRecordIdentifier(identifier); - let internalModel = internalModelFactoryFor(this).peek(stableIdentifier); - // TODO we used to check if the record was destroyed here - return internalModel!.referenceFor(null, key); - } - - /** - * Manages setting setting up the recordData returned by createRecordDataFor - * - * @method _createRecordData - * @internal - */ - // TODO move this to InstanceCache - _createRecordData(identifier: StableRecordIdentifier): RecordData { - const recordData = this.createRecordDataFor(identifier.type, identifier.id, identifier.lid, this._storeWrapper); - setRecordDataFor(identifier, recordData); - // TODO this is invalid for v2 recordData but required - // for v1 recordData. Remember to remove this once the - // RecordData manager handles converting recordData to identifier - setRecordIdentifier(recordData, identifier); - return recordData; - } - /** * Instantiation hook allowing applications or addons to configure the store * to utilize a custom RecordData implementation. @@ -2702,39 +2241,6 @@ class CoreStore extends Service { assert(`Expected store.createRecordDataFor to be implemented but it wasn't`); } - /** - * @internal - */ - - // TODO move this to InstanceCache private property - __recordDataFor(resource: RecordIdentifier) { - const identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); - return this.recordDataFor(identifier, false); - } - - /** - * @internal - */ - // TODO move this to InstanceCache - recordDataFor(identifier: StableRecordIdentifier | { type: string }, isCreate: boolean): RecordData { - let recordData: RecordData; - if (isCreate === true) { - // TODO remove once InternalModel is no longer essential to internal state - // and just build a new identifier directly - let internalModel = internalModelFactoryFor(this).build({ type: identifier.type, id: null }); - let stableIdentifier = internalModel.identifier; - recordData = this._instanceCache.getRecordData(stableIdentifier); - recordData.clientDidCreate(); - this.recordArrayManager.recordDidChange(stableIdentifier); - } else { - // TODO remove once InternalModel is no longer essential to internal state - internalModelFactoryFor(this).lookup(identifier as StableRecordIdentifier); - recordData = this._instanceCache.getRecordData(identifier as StableRecordIdentifier); - } - - return recordData; - } - /** `normalize` converts a json payload into the normalized form that [push](../methods/push?anchor=push) expects. @@ -2762,9 +2268,7 @@ class CoreStore extends Service { } 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 ${inspect( - modelName - )}`, + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, typeof modelName === 'string' ); let normalizedModelName = normalizeModelName(modelName); @@ -2777,23 +2281,6 @@ class CoreStore extends Service { return serializer.normalize(model, payload); } - // ............... - // . DESTRUCTION . - // ............... - - /** - * TODO remove test usage - * - * @internal - */ - _internalModelsFor(modelName: string) { - return internalModelFactoryFor(this).modelMapFor(modelName); - } - - // ...................... - // . PER-TYPE ADAPTERS - // ...................... - /** Returns an instance of the adapter for a given type. For example, `adapterFor('person')` will return an instance of @@ -2867,10 +2354,6 @@ class CoreStore extends Service { assert(`No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`); } - // .............................. - // . RECORD CHANGE NOTIFICATION . - // .............................. - /** Returns an instance of the serializer for a given type. For example, `serializerFor('person')` will return an instance of @@ -2990,7 +2473,7 @@ class CoreStore extends Service { } } -export default CoreStore; +export default Store; let assertDestroyingStore: Function; let assertDestroyedStoreOnly: Function; @@ -3010,47 +2493,6 @@ if (DEBUG) { }; } -/** - * Flag indicating whether all inverse records are available - * - * true if the inverse exists and is loaded (not empty) - * true if there is no inverse - * false if the inverse exists and is not loaded (empty) - * - * @internal - * @return {boolean} - */ -function areAllInverseRecordsLoaded(store: CoreStore, resource: JsonApiRelationship): boolean { - const cache = store.identifierCache; - - if (Array.isArray(resource.data)) { - // treat as collection - // check for unloaded records - let hasEmptyRecords = resource.data.reduce((hasEmptyModel, resourceIdentifier) => { - return hasEmptyModel || internalModelForRelatedResource(store, cache, resourceIdentifier).isEmpty; - }, false); - - return !hasEmptyRecords; - } else { - // treat as single resource - if (!resource.data) { - return true; - } else { - const internalModel = internalModelForRelatedResource(store, cache, resource.data); - return !internalModel.isEmpty; - } - } -} - -function internalModelForRelatedResource( - store: CoreStore, - cache: IdentifierCache, - resource: ResourceIdentifierObject -): InternalModel { - const identifier = cache.getOrCreateRecordIdentifier(resource); - return store._internalModelForResource(identifier); -} - function isMaybeIdentifier( maybeIdentifier: string | ResourceIdentifierObject ): maybeIdentifier is ResourceIdentifierObject { @@ -3062,7 +2504,7 @@ function isMaybeIdentifier( ); } -function assertIdentifierHasId( +export function assertIdentifierHasId( identifier: StableRecordIdentifier ): asserts identifier is StableExistingRecordIdentifier { assert(`Attempted to schedule a fetch for a record without an id.`, identifier.id !== null); diff --git a/packages/store/addon/-private/system/errors-utils.js b/packages/store/addon/-private/errors-utils.js similarity index 100% rename from packages/store/addon/-private/system/errors-utils.js rename to packages/store/addon/-private/errors-utils.js diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/fetch-manager.ts similarity index 95% rename from packages/store/addon/-private/system/fetch-manager.ts rename to packages/store/addon/-private/fetch-manager.ts index cdb01958679..cc4e42764a2 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/fetch-manager.ts @@ -8,26 +8,25 @@ import { DEBUG } from '@glimmer/env'; import { default as RSVP, resolve } from 'rsvp'; import { DEPRECATE_RSVP_PROMISE } from '@ember-data/private-build-infra/deprecations'; - -import type { CollectionResourceDocument, SingleResourceDocument } from '../ts-interfaces/ember-data-json-api'; -import type { FindRecordQuery, Request, SaveRecordMutation } from '../ts-interfaces/fetch-manager'; +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 '../ts-interfaces/identifier'; -import type { MinimumSerializerInterface } from '../ts-interfaces/minimum-serializer-interface'; -import { FindOptions } from '../ts-interfaces/store'; -import type { Dict } from '../ts-interfaces/utils'; +} from '@ember-data/types/q/identifier'; +import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface'; +import type { FindOptions } from '@ember-data/types/q/store'; +import type { Dict } from '@ember-data/types/q/utils'; + import coerceId from './coerce-id'; -import type CoreStore from './core-store'; +import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common'; +import type Store from './core-store'; import { errorsArrayToHash } from './errors-utils'; import ShimModelClass from './model/shim-model-class'; import RequestCache from './request-cache'; -import type { PrivateSnapshot } from './snapshot'; +import { normalizeResponseHelper } from './serializer-response'; import Snapshot from './snapshot'; -import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './store/common'; -import { normalizeResponseHelper } from './store/serializer-response'; import WeakCache from './weak-cache'; function payloadIsNotBlank(adapterPayload): boolean { @@ -40,7 +39,7 @@ function payloadIsNotBlank(adapterPayload): boolean { type AdapterErrors = Error & { errors?: string[]; isAdapterError?: true }; type SerializerWithParseErrors = MinimumSerializerInterface & { - extractErrors?(store: CoreStore, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): any; + extractErrors?(store: Store, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): any; }; export const SaveOp: unique symbol = Symbol('SaveOp'); @@ -78,7 +77,7 @@ export default class FetchManager { // fetches pending in the runloop, waiting to be coalesced declare _pendingFetch: Map; - constructor(private _store: CoreStore) { + constructor(private _store: Store) { // used to keep track of all the find requests that need to be coalesced this._pendingFetch = new Map(); this._pendingSave = []; @@ -128,9 +127,7 @@ export default class FetchManager { let adapter = this._store.adapterFor(identifier.type); let operation = options[SaveOp]; - // TODO We have to cast due to our reliance on this private property - // this will be refactored away once we change our pending API to be identifier based - let internalModel = (snapshot as unknown as PrivateSnapshot)._internalModel; + let internalModel = snapshot._internalModel; let modelName = snapshot.modelName; let store = this._store; let modelClass = store.modelFor(modelName); @@ -440,7 +437,7 @@ export default class FetchManager { _findMany( adapter: any, - store: CoreStore, + store: Store, modelName: string, snapshots: Snapshot[], identifiers: RecordIdentifier[], diff --git a/packages/store/addon/-private/finders.js b/packages/store/addon/-private/finders.js new file mode 100644 index 00000000000..1a4943b66ca --- /dev/null +++ b/packages/store/addon/-private/finders.js @@ -0,0 +1,107 @@ +import { assert } from '@ember/debug'; + +import { Promise } from 'rsvp'; + +import { guardDestroyedStore } from './common'; +import { normalizeResponseHelper } from './serializer-response'; + +/** + @module @ember-data/store +*/ + +function payloadIsNotBlank(adapterPayload) { + if (Array.isArray(adapterPayload)) { + return true; + } else { + return Object.keys(adapterPayload || {}).length; + } +} + +export function _findAll(adapter, store, modelName, options) { + let modelClass = store.modelFor(modelName); // adapter.findAll depends on the class + let recordArray = store.peekAll(modelName); + let snapshotArray = recordArray._createSnapshot(options); + let promise = Promise.resolve().then(() => adapter.findAll(store, modelClass, null, snapshotArray)); + let label = 'DS: Handle Adapter#findAll of ' + modelClass; + + promise = guardDestroyedStore(promise, store, label); + + return promise.then( + (adapterPayload) => { + assert( + `You made a 'findAll' request for '${modelName}' records, 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, 'findAll'); + + store._push(payload); + store.recordArrayManager._didUpdateAll(modelName); + + return recordArray; + }, + null, + 'DS: Extract payload of findAll ${modelName}' + ); +} + +export function _query(adapter, store, modelName, query, recordArray, options) { + let modelClass = store.modelFor(modelName); // adapter.query needs the class + + recordArray = recordArray || store.recordArrayManager.createAdapterPopulatedRecordArray(modelName, query); + let promise = Promise.resolve().then(() => adapter.query(store, modelClass, query, recordArray, options)); + + let label = `DS: Handle Adapter#query of ${modelName}`; + promise = guardDestroyedStore(promise, store, label); + + return promise.then( + (adapterPayload) => { + let serializer = store.serializerFor(modelName); + let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'query'); + let identifiers = store._push(payload); + + assert( + 'The response to store.query is expected to be an array but it was a single record. Please wrap your response in an array or use `store.queryRecord` to query for a single record.', + Array.isArray(identifiers) + ); + if (recordArray) { + recordArray._setIdentifiers(identifiers, payload); + } else { + recordArray = store.recordArrayManager.createAdapterPopulatedRecordArray( + modelName, + query, + identifiers, + payload + ); + } + + return recordArray; + }, + null, + `DS: Extract payload of query ${modelName}` + ); +} + +export function _queryRecord(adapter, store, modelName, query, options) { + let modelClass = store.modelFor(modelName); // adapter.queryRecord needs the class + let promise = Promise.resolve().then(() => adapter.queryRecord(store, modelClass, query, options)); + + let label = `DS: Handle Adapter#queryRecord of ${modelName}`; + promise = guardDestroyedStore(promise, store, label); + + return promise.then( + (adapterPayload) => { + let serializer = store.serializerFor(modelName); + let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'queryRecord'); + + assert( + `Expected the primary data returned by the serializer for a 'queryRecord' response to be a single object or null but instead it was an array.`, + !Array.isArray(payload.data) + ); + + return store._push(payload); + }, + null, + `DS: Extract payload of queryRecord ${modelName}` + ); +} diff --git a/packages/store/addon/-private/identifer-debug-consts.ts b/packages/store/addon/-private/identifer-debug-consts.ts new file mode 100644 index 00000000000..fd5842acf90 --- /dev/null +++ b/packages/store/addon/-private/identifer-debug-consts.ts @@ -0,0 +1,3 @@ +// 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/addon/-private/identifiers/cache.ts b/packages/store/addon/-private/identifier-cache.ts similarity index 95% rename from packages/store/addon/-private/identifiers/cache.ts rename to packages/store/addon/-private/identifier-cache.ts index 45072baca1a..d5043a35757 100644 --- a/packages/store/addon/-private/identifiers/cache.ts +++ b/packages/store/addon/-private/identifier-cache.ts @@ -4,10 +4,7 @@ import { assert, warn } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import coerceId from '../system/coerce-id'; -import normalizeModelName from '../system/normalize-model-name'; -import WeakCache from '../system/weak-cache'; -import type { ExistingResourceObject, ResourceIdentifierObject } from '../ts-interfaces/ember-data-json-api'; +import type { ExistingResourceObject, ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; import type { ForgetMethod, GenerationMethod, @@ -18,12 +15,27 @@ import type { ResourceData, StableRecordIdentifier, UpdateMethod, -} from '../ts-interfaces/identifier'; -import { DEBUG_CLIENT_ORIGINATED, DEBUG_IDENTIFIER_BUCKET } from '../ts-interfaces/identifier'; -import type { ConfidentDict } from '../ts-interfaces/utils'; -import isNonEmptyString from '../utils/is-non-empty-string'; -import isStableIdentifier, { markStableIdentifier, unmarkStableIdentifier } from './is-stable-identifier'; -import uuidv4 from './utils/uuid-v4'; +} from '@ember-data/types/q/identifier'; +import type { ConfidentDict } from '@ember-data/types/q/utils'; + +import coerceId from './coerce-id'; +import { DEBUG_CLIENT_ORIGINATED, DEBUG_IDENTIFIER_BUCKET } from './identifer-debug-consts'; +import normalizeModelName from './normalize-model-name'; +import isNonEmptyString from './utils/is-non-empty-string'; +import WeakCache from './weak-cache'; + +const IDENTIFIERS = new WeakSet(); + +export function isStableIdentifier(identifier: Object): identifier is StableRecordIdentifier { + return IDENTIFIERS.has(identifier); +} + +const isFastBoot = typeof FastBoot !== 'undefined'; +const _crypto: Crypto = isFastBoot ? (FastBoot.require('crypto') as Crypto) : window.crypto; + +function uuidv4(): string { + return _crypto.randomUUID(); +} function freeze(obj: T): T { if (typeof Object.freeze === 'function') { @@ -75,7 +87,7 @@ function defaultGenerationMethod(data: ResourceData | { type: string }, bucket: let { type, id } = data; // TODO: add test for id not a string if (isNonEmptyString(coerceId(id))) { - return `@ember-data:lid-${normalizeModelName(type)}-${id}`; + return `@lid:${normalizeModelName(type)}-${id}`; } } return uuidv4(); @@ -440,7 +452,7 @@ export class IdentifierCache { let index = keyOptions._allIdentifiers.indexOf(identifier); keyOptions._allIdentifiers.splice(index, 1); - unmarkStableIdentifier(identifierObject); + IDENTIFIERS.delete(identifierObject); this._forget(identifier, 'record'); } @@ -476,7 +488,7 @@ function makeStableRecordIdentifier( id, type, }; - markStableIdentifier(recordIdentifier); + IDENTIFIERS.add(recordIdentifier); if (DEBUG) { // we enforce immutability in dev @@ -498,7 +510,7 @@ function makeStableRecordIdentifier( }; wrapper[DEBUG_CLIENT_ORIGINATED] = clientOriginated; wrapper[DEBUG_IDENTIFIER_BUCKET] = bucket; - markStableIdentifier(wrapper); + IDENTIFIERS.add(wrapper); DEBUG_MAP.set(wrapper, recordIdentifier); wrapper = freeze(wrapper); return wrapper; diff --git a/packages/store/addon/-private/identifiers/is-stable-identifier.ts b/packages/store/addon/-private/identifiers/is-stable-identifier.ts deleted file mode 100644 index fd06d00e523..00000000000 --- a/packages/store/addon/-private/identifiers/is-stable-identifier.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -/** - @module @ember-data/store -*/ - -const IDENTIFIERS = new WeakSet(); - -export default function isStableIdentifier(identifier: Object): identifier is StableRecordIdentifier { - return IDENTIFIERS.has(identifier); -} - -export function markStableIdentifier(identifier: Object) { - IDENTIFIERS.add(identifier); -} - -export function unmarkStableIdentifier(identifier: Object) { - IDENTIFIERS.delete(identifier); -} diff --git a/packages/store/addon/-private/identifiers/utils/uuid-v4.ts b/packages/store/addon/-private/identifiers/utils/uuid-v4.ts deleted file mode 100644 index 162ec7b597c..00000000000 --- a/packages/store/addon/-private/identifiers/utils/uuid-v4.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - @module @ember-data/store -*/ - -const CRYPTO = (() => { - const hasWindow = typeof window !== 'undefined'; - const isFastBoot = typeof FastBoot !== 'undefined'; - - if (isFastBoot) { - return { - getRandomValues(buffer: Uint8Array) { - try { - return (FastBoot as FastBoot).require('crypto').randomFillSync(buffer); - } catch (err) { - throw new Error( - 'Using createRecord in Fastboot requires you to add the "crypto" package to "fastbootDependencies" in your package.json' - ); - } - }, - }; - } else if (hasWindow && typeof window.crypto !== 'undefined') { - return window.crypto; - } else { - throw new Error('ember-data: Cannot find a valid way to generate local identifiers'); - } -})(); - -// we might be able to optimize this by requesting more bytes than we need at a time -function rng() { - // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto - let rnds8 = new Uint8Array(16); - - return CRYPTO.getRandomValues(rnds8); -} - -/* - * Convert array of 16 byte values to UUID string format of the form: - * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX - */ -const byteToHex: string[] = []; -for (let i = 0; i < 256; ++i) { - byteToHex[i] = (i + 0x100).toString(16).substr(1); -} - -function bytesToUuid(buf) { - let 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]], - bth[buf[1]], - bth[buf[2]], - bth[buf[3]], - '-', - bth[buf[4]], - bth[buf[5]], - '-', - bth[buf[6]], - bth[buf[7]], - '-', - bth[buf[8]], - bth[buf[9]], - '-', - bth[buf[10]], - bth[buf[11]], - bth[buf[12]], - bth[buf[13]], - bth[buf[14]], - bth[buf[15]], - ].join(''); -} - -export default function uuidv4(): string { - let rnds = rng(); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = (rnds[6] & 0x0f) | 0x40; - rnds[8] = (rnds[8] & 0x3f) | 0x80; - - return bytesToUuid(rnds); -} diff --git a/packages/store/addon/-private/system/identity-map.ts b/packages/store/addon/-private/identity-map.ts similarity index 94% rename from packages/store/addon/-private/system/identity-map.ts rename to packages/store/addon/-private/identity-map.ts index cd5268ff52f..d14472c0d7a 100644 --- a/packages/store/addon/-private/system/identity-map.ts +++ b/packages/store/addon/-private/identity-map.ts @@ -1,4 +1,5 @@ -import type { ConfidentDict } from '../ts-interfaces/utils'; +import type { ConfidentDict } from '@ember-data/types/q/utils'; + import InternalModelMap from './internal-model-map'; /** diff --git a/packages/store/addon/-private/index.ts b/packages/store/addon/-private/index.ts index 72ed97cac43..6233a059c0f 100644 --- a/packages/store/addon/-private/index.ts +++ b/packages/store/addon/-private/index.ts @@ -2,37 +2,38 @@ @module @ember-data/store */ -export { default as Store, storeFor } from './system/core-store'; +export { default as Store, storeFor } from './core-store'; -export { recordIdentifierFor } from './system/store/internal-model-factory'; +export { recordIdentifierFor } from './internal-model-factory'; -export { default as Snapshot } from './system/snapshot'; +export { default as Snapshot } from './snapshot'; export { setIdentifierGenerationMethod, setIdentifierUpdateMethod, setIdentifierForgetMethod, setIdentifierResetMethod, -} from './identifiers/cache'; +} from './identifier-cache'; -export { default as normalizeModelName } from './system/normalize-model-name'; -export { default as coerceId } from './system/coerce-id'; +export { default as normalizeModelName } from './normalize-model-name'; +export { default as coerceId } from './coerce-id'; -export { errorsHashToArray, errorsArrayToHash } from './system/errors-utils'; +export { errorsHashToArray, errorsArrayToHash } from './errors-utils'; // `ember-data-model-fragments` relies on `InternalModel` -export { default as InternalModel } from './system/model/internal-model'; +export { default as InternalModel } from './model/internal-model'; -export { PromiseArray, PromiseObject, deprecatedPromiseObject } from './system/promise-proxies'; +export { PromiseArray, PromiseObject, deprecatedPromiseObject } from './promise-proxies'; -export { RecordArray, AdapterPopulatedRecordArray } from './system/record-arrays'; +export { default as RecordArray } from './record-arrays/record-array'; +export { default as AdapterPopulatedRecordArray } from './record-arrays/adapter-populated-record-array'; -export { default as RecordArrayManager } from './system/record-array-manager'; +export { default as RecordArrayManager } from './record-array-manager'; // // Used by tests -export { default as SnapshotRecordArray } from './system/snapshot-record-array'; +export { default as SnapshotRecordArray } from './snapshot-record-array'; // New -export { default as recordDataFor, removeRecordDataFor } from './system/record-data-for'; -export { default as RecordDataStoreWrapper } from './system/store/record-data-store-wrapper'; +export { default as recordDataFor, removeRecordDataFor } from './record-data-for'; +export { default as RecordDataStoreWrapper } from './record-data-store-wrapper'; -export { default as WeakCache } from './system/weak-cache'; +export { default as WeakCache } from './weak-cache'; diff --git a/packages/store/addon/-private/instance-cache.ts b/packages/store/addon/-private/instance-cache.ts index c3c3060ed2b..9de6d9e8d88 100644 --- a/packages/store/addon/-private/instance-cache.ts +++ b/packages/store/addon/-private/instance-cache.ts @@ -1,27 +1,67 @@ import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; -import type { CreateRecordProperties } from './system/core-store'; -import CoreStore from './system/core-store'; -import Snapshot from './system/snapshot'; -import type { StableRecordIdentifier } from './ts-interfaces/identifier'; -import { RecordData } from './ts-interfaces/record-data'; -import type { RecordInstance } from './ts-interfaces/record-instance'; -import { FindOptions } from './ts-interfaces/store'; +import { resolve } from 'rsvp'; + +import type { ExistingResourceObject, ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; +import type { + RecordIdentifier, + StableExistingRecordIdentifier, + StableRecordIdentifier, +} from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; +import type { FindOptions } from '@ember-data/types/q/store'; + +import coerceId, { ensureStringId } from './coerce-id'; +import type { CreateRecordProperties } from './core-store'; +import type Store from './core-store'; +import { assertIdentifierHasId } from './core-store'; +import { internalModelFactoryFor, setRecordIdentifier } from './internal-model-factory'; +import InternalModel from './model/internal-model'; +import RecordReference from './model/record-reference'; +import normalizeModelName from './normalize-model-name'; +import recordDataFor, { setRecordDataFor } from './record-data-for'; +import RecordDataStoreWrapper from './record-data-store-wrapper'; +import Snapshot from './snapshot'; +import constructResource from './utils/construct-resource'; +import WeakCache from './weak-cache'; + +const RECORD_REFERENCES = new WeakCache(DEBUG ? 'reference' : ''); +export const StoreMap = new WeakCache(DEBUG ? 'store' : ''); + +export function storeFor(record: RecordInstance): Store | undefined { + const store = StoreMap.get(record); + + assert( + `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`, + store + ); + return store; +} type Caches = { record: WeakMap; recordData: WeakMap; }; export class InstanceCache { - declare store: CoreStore; + declare store: Store; + declare _storeWrapper: RecordDataStoreWrapper; #instances: Caches = { record: new WeakMap(), recordData: new WeakMap(), }; - constructor(store: CoreStore) { + constructor(store: Store) { this.store = store; + + this._storeWrapper = new RecordDataStoreWrapper(this.store); + this.__recordDataFor = this.__recordDataFor.bind(this); + + RECORD_REFERENCES._generator = (identifier) => { + return new RecordReference(this.store, identifier); + }; } peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined; peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): RecordData | undefined; @@ -70,19 +110,117 @@ export class InstanceCache { internalModel.setId(properties.id); } - record = this.store._instantiateRecord(this.getRecordData(identifier), identifier, properties); + record = this._instantiateRecord(this.getRecordData(identifier), identifier, properties); this.set({ identifier, bucket: 'record', value: record }); } return record; } + getReference(identifier: StableRecordIdentifier) { + return RECORD_REFERENCES.lookup(identifier); + } + + _fetchDataIfNeededForIdentifier( + identifier: StableRecordIdentifier, + options: FindOptions = {} + ): Promise { + const internalModel = this.getInternalModel(identifier); + + // pre-loading will change the isEmpty value + // TODO stpre this state somewhere other than InternalModel + const { isEmpty, isLoading } = internalModel; + + if (options.preload) { + this.store._backburner.join(() => { + internalModel.preloadData(options.preload); + }); + } + + let promise: Promise; + if (isEmpty) { + assertIdentifierHasId(identifier); + + promise = this.store._fetchManager.scheduleFetch(identifier, options); + } else if (isLoading) { + promise = this.store._fetchManager.getPendingFetch(identifier, options)!; + assert(`Expected to find a pending request for a record in the loading state, but found none`, promise); + } else { + promise = resolve(identifier); + } + + return promise; + } + + _instantiateRecord( + recordData: RecordData, + identifier: StableRecordIdentifier, + properties?: { [key: string]: unknown } + ) { + // assert here + if (properties !== undefined) { + assert( + `You passed '${typeof properties}' as properties for record creation instead of an object.`, + typeof properties === 'object' && properties !== null + ); + + const { type } = identifier; + + // convert relationship Records to RecordDatas before passing to RecordData + let defs = this.store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); + + if (defs !== null) { + let keys = Object.keys(properties); + let relationshipValue; + + for (let i = 0; i < keys.length; i++) { + let prop = keys[i]; + let def = defs[prop]; + + if (def !== undefined) { + if (def.kind === 'hasMany') { + if (DEBUG) { + assertRecordsPassedToHasMany(properties[prop] as RecordInstance[]); + } + relationshipValue = extractRecordDatasFromRecords(properties[prop] as RecordInstance[]); + } else { + relationshipValue = extractRecordDataFromRecord(properties[prop] as RecordInstance); + } + + properties[prop] = relationshipValue; + } + } + } + } + + // TODO guard against initRecordOptions no being there + let createOptions = recordData._initRecordCreateOptions(properties); + //TODO Igor pass a wrapper instead of RD + let record = this.store.instantiateRecord( + identifier, + createOptions, + // eslint-disable-next-line @typescript-eslint/unbound-method + this.__recordDataFor, + this.store._notificationManager + ); + setRecordIdentifier(record, identifier); + setRecordDataFor(record, recordData); + StoreMap.set(record, this.store); + return record; + } + + _teardownRecord(record: RecordInstance) { + StoreMap.delete(record); + // TODO remove identifier:record cache link + this.store.teardownRecord(record); + } + removeRecord(identifier: StableRecordIdentifier): boolean { let record = this.peek({ identifier, bucket: 'record' }); if (record) { this.#instances.record.delete(identifier); - this.store._teardownRecord(record); + this._teardownRecord(record); } return !!record; @@ -93,7 +231,7 @@ export class InstanceCache { let recordData = this.peek({ identifier, bucket: 'recordData' }); if (!recordData) { - recordData = this.store._createRecordData(identifier); + recordData = this._createRecordData(identifier); this.#instances.recordData.set(identifier, recordData); this.getInternalModel(identifier).hasRecordData = true; } @@ -103,10 +241,147 @@ export class InstanceCache { // TODO move InternalModel cache into InstanceCache getInternalModel(identifier: StableRecordIdentifier) { - return this.store._internalModelForResource(identifier); + return this._internalModelForResource(identifier); } createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot { return new Snapshot(options, identifier, this.store); } + + __recordDataFor(resource: RecordIdentifier) { + const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); + return this.recordDataFor(identifier, false); + } + + // TODO move this to InstanceCache + _createRecordData(identifier: StableRecordIdentifier): RecordData { + const recordData = this.store.createRecordDataFor( + identifier.type, + identifier.id, + identifier.lid, + this._storeWrapper + ); + setRecordDataFor(identifier, recordData); + // TODO this is invalid for v2 recordData but required + // for v1 recordData. Remember to remove this once the + // RecordData manager handles converting recordData to identifier + setRecordIdentifier(recordData, identifier); + return recordData; + } + + // TODO string candidate for early elimination + _internalModelForResource(resource: ResourceIdentifierObject): InternalModel { + return internalModelFactoryFor(this.store).getByResource(resource); + } + + setRecordId(modelName: string, newId: string, clientId: string) { + internalModelFactoryFor(this.store).setRecordId(modelName, newId, clientId); + } + + _load(data: ExistingResourceObject): StableExistingRecordIdentifier { + // TODO type should be pulled from the identifier for debug + 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) + ); + + // TODO this should determine identifier via the cache before making assumptions + const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid)); + const maybeIdentifier = this.store.identifierCache.peekRecordIdentifier(resource); + + let internalModel = internalModelFactoryFor(this.store).lookup(resource, data); + + // 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 + const isLoading = internalModel.isLoading || (!internalModel.isLoaded && maybeIdentifier); + const isUpdate = internalModel.isEmpty === false && !isLoading; + + // exclude store.push (root.empty) case + let identifier = internalModel.identifier; + if (isUpdate || isLoading) { + let updatedIdentifier = this.store.identifierCache.updateRecordIdentifier(identifier, data); + + if (updatedIdentifier !== identifier) { + // we encountered a merge of identifiers in which + // two identifiers (and likely two internalModels) + // existed for the same resource. Now that we have + // determined the correct identifier to use, make sure + // that we also use the correct internalModel. + identifier = updatedIdentifier; + internalModel = internalModelFactoryFor(this.store).lookup(identifier); + } + } + + internalModel.setupData(data); + + if (!isUpdate) { + this.store.recordArrayManager.recordDidChange(identifier); + } + + return identifier as StableExistingRecordIdentifier; + } + + recordDataFor(identifier: StableRecordIdentifier | { type: string }, isCreate: boolean): RecordData { + let recordData: RecordData; + if (isCreate === true) { + // TODO remove once InternalModel is no longer essential to internal state + // and just build a new identifier directly + let internalModel = internalModelFactoryFor(this.store).build({ type: identifier.type, id: null }); + let stableIdentifier = internalModel.identifier; + recordData = this.getRecordData(stableIdentifier); + recordData.clientDidCreate(); + this.store.recordArrayManager.recordDidChange(stableIdentifier); + } else { + // TODO remove once InternalModel is no longer essential to internal state + internalModelFactoryFor(this.store).lookup(identifier as StableRecordIdentifier); + recordData = this.getRecordData(identifier as StableRecordIdentifier); + } + + return recordData; + } +} + +function assertRecordsPassedToHasMany(records: RecordInstance[]) { + 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 + .map((r) => `${typeof r}`) + .join(', ')}`, + (function () { + return records.every((record) => Object.prototype.hasOwnProperty.call(record, '_internalModel') === true); + })() + ); +} + +function extractRecordDatasFromRecords(records: RecordInstance[]): RecordData[] { + return records.map(extractRecordDataFromRecord) as RecordData[]; +} +type PromiseProxyRecord = { then(): void; get(str: 'content'): RecordInstance | null | undefined }; + +function extractRecordDataFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { + if (!recordOrPromiseRecord) { + return null; + } + + if (isPromiseRecord(recordOrPromiseRecord)) { + let content = recordOrPromiseRecord.get && recordOrPromiseRecord.get('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 + ); + return content ? recordDataFor(content) : null; + } + + return recordDataFor(recordOrPromiseRecord); +} + +function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord { + return !!record.then; } diff --git a/packages/store/addon/-private/system/store/internal-model-factory.ts b/packages/store/addon/-private/internal-model-factory.ts similarity index 93% rename from packages/store/addon/-private/system/store/internal-model-factory.ts rename to packages/store/addon/-private/internal-model-factory.ts index ebf2398c1e9..ade102b82e3 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/internal-model-factory.ts @@ -1,27 +1,28 @@ import { assert, warn } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import type { IdentifierCache } from '../../identifiers/cache'; import type { ExistingResourceObject, NewResourceIdentifierObject, ResourceIdentifierObject, -} from '../../ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordData } from '../../ts-interfaces/record-data'; -import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import constructResource from '../../utils/construct-resource'; -import type CoreStore from '../core-store'; -import IdentityMap from '../identity-map'; -import type InternalModelMap from '../internal-model-map'; -import InternalModel from '../model/internal-model'; -import WeakCache from '../weak-cache'; +} from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; + +import type Store from './core-store'; +import type { IdentifierCache } from './identifier-cache'; +import IdentityMap from './identity-map'; +import type InternalModelMap from './internal-model-map'; +import InternalModel from './model/internal-model'; +import constructResource from './utils/construct-resource'; +import WeakCache from './weak-cache'; /** @module @ember-data/store */ -const FactoryCache = new WeakCache(DEBUG ? 'internal-model-factory' : ''); -FactoryCache._generator = (store: CoreStore) => { +const FactoryCache = new WeakCache(DEBUG ? 'internal-model-factory' : ''); +FactoryCache._generator = (store: Store) => { return new InternalModelFactory(store); }; type NewResourceInfo = { type: string; id: string | null }; @@ -80,7 +81,7 @@ export function setRecordIdentifier(record: RecordInstance | RecordData, identif RecordCache.set(record, identifier); } -export function internalModelFactoryFor(store: CoreStore): InternalModelFactory { +export function internalModelFactoryFor(store: Store): InternalModelFactory { return FactoryCache.lookup(store); } @@ -95,9 +96,9 @@ export function internalModelFactoryFor(store: CoreStore): InternalModelFactory export default class InternalModelFactory { declare _identityMap: IdentityMap; declare identifierCache: IdentifierCache; - declare store: CoreStore; + declare store: Store; - constructor(store: CoreStore) { + constructor(store: Store) { this.store = store; this.identifierCache = store.identifierCache; this.identifierCache.__configureMerge((identifier, matchedIdentifier, resourceData) => { diff --git a/packages/store/addon/-private/system/internal-model-map.ts b/packages/store/addon/-private/internal-model-map.ts similarity index 95% rename from packages/store/addon/-private/system/internal-model-map.ts rename to packages/store/addon/-private/internal-model-map.ts index 8c3aada96d3..74a98d32eba 100644 --- a/packages/store/addon/-private/system/internal-model-map.ts +++ b/packages/store/addon/-private/internal-model-map.ts @@ -1,7 +1,8 @@ import { assert } from '@ember/debug'; -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type { ConfidentDict } from '../ts-interfaces/utils'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { ConfidentDict } from '@ember-data/types/q/utils'; + import InternalModel from './model/internal-model'; /** diff --git a/packages/store/addon/-private/model/internal-model.ts b/packages/store/addon/-private/model/internal-model.ts new file mode 100644 index 00000000000..27c1b413ac6 --- /dev/null +++ b/packages/store/addon/-private/model/internal-model.ts @@ -0,0 +1,602 @@ +import { assert } from '@ember/debug'; +import { _backburner as emberBackburner, cancel, run } from '@ember/runloop'; +import { DEBUG } from '@glimmer/env'; + +import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra'; +import type { DSModel } from '@ember-data/types/q/ds-model'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data'; +import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; + +import type Store from '../core-store'; +import { errorsHashToArray } from '../errors-utils'; +import { internalModelFactoryFor } from '../internal-model-factory'; + +/** + @module @ember-data/store +*/ + +function isDSModel(record: RecordInstance | null): record is DSModel { + return ( + HAS_MODEL_PACKAGE && + !!record && + 'constructor' in record && + 'isModel' in record.constructor && + record.constructor.isModel === true + ); +} + +export default class InternalModel { + declare _id: string | null; + declare modelName: string; + declare clientId: string; + declare hasRecordData: boolean; + declare _isDestroyed: boolean; + declare isError: boolean; + declare _pendingRecordArrayManagerFlush: boolean; + declare _isDematerializing: boolean; + declare _doNotDestroy: boolean; + declare isDestroying: boolean; + declare _isUpdatingId: boolean; + declare _deletedRecordWasNew: boolean; + + // Not typed yet + declare _scheduledDestroy: any; + declare _modelClass: any; + declare __recordArrays: any; + declare error: any; + declare store: Store; + declare identifier: StableRecordIdentifier; + declare hasRecord: boolean; + + constructor(store: Store, identifier: StableRecordIdentifier) { + this.store = store; + this.identifier = identifier; + this._id = identifier.id; + this._isUpdatingId = false; + this.modelName = identifier.type; + this.clientId = identifier.lid; + this.hasRecord = false; + + this.hasRecordData = false; + + this._isDestroyed = false; + this._doNotDestroy = false; + this.isError = false; + this._pendingRecordArrayManagerFlush = false; // used by the recordArrayManager + + // During dematerialization we don't want to rematerialize the record. The + // reason this might happen is that dematerialization removes records from + // record arrays, and Ember arrays will always `objectAt(0)` and + // `objectAt(len - 1)` to test whether or not `firstObject` or `lastObject` + // have changed. + this._isDematerializing = false; + this._scheduledDestroy = null; + + this.error = null; + + // caches for lazy getters + this._modelClass = null; + this.__recordArrays = null; + + this.error = null; + } + + get id(): string | null { + return this.identifier.id; + } + set id(value: string | null) { + if (value !== this._id) { + let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; + // TODO potentially this needs to handle merged result + this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier); + this.notifyPropertyChange('id'); + } + } + + get modelClass() { + if (this.store.modelFor) { + return this._modelClass || (this._modelClass = this.store.modelFor(this.modelName)); + } + } + + get _recordData(): RecordData { + return this.store._instanceCache.getRecordData(this.identifier); + } + + isHiddenFromRecordArrays() { + // During dematerialization we don't want to rematerialize the record. + // recordWasDeleted can cause other records to rematerialize because it + // removes the internal model from the array and Ember arrays will always + // `objectAt(0)` and `objectAt(len -1)` to check whether `firstObject` or + // `lastObject` have changed. When this happens we don't want those + // models to rematerialize their records. + + // eager checks to avoid instantiating record data if we are empty or loading + if (this.isEmpty) { + return true; + } + + if (this.isLoading) { + return false; + } + + let isRecordFullyDeleted = this._isRecordFullyDeleted(); + return this._isDematerializing || this.hasScheduledDestroy() || this.isDestroyed || isRecordFullyDeleted; + } + + _isRecordFullyDeleted(): boolean { + if (this._recordData.isDeletionCommitted && this._recordData.isDeletionCommitted()) { + return true; + } else if ( + this._recordData.isNew && + this._recordData.isDeleted && + this._recordData.isNew() && + this._recordData.isDeleted() + ) { + return true; + } else { + return false; + } + } + + isDeleted(): boolean { + if (this._recordData.isDeleted) { + return this._recordData.isDeleted(); + } else { + return false; + } + } + + isNew(): boolean { + if (this.hasRecordData && this._recordData.isNew) { + return this._recordData.isNew(); + } else { + return false; + } + } + + get isEmpty(): boolean { + return !this.hasRecordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false; + } + + get isLoading() { + const req = this.store.getRequestStateService(); + const { identifier } = this; + // const fulfilled = req.getLastRequestForRecord(identifier); + + return ( + !this.isLoaded && + // fulfilled === null && + req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query') + ); + } + + get isLoaded() { + // if we are new we must consider ourselves loaded + if (this.isNew()) { + return true; + } + // even if we have a past request, if we are now empty we are not loaded + // typically this is true after an unloadRecord call + + // if we are not empty, not new && we have a fulfilled request then we are loaded + // we should consider allowing for something to be loaded that is simply "not empty". + // which is how RecordState currently handles this case; however, RecordState is buggy + // in that it does not account for unloading. + return !this.isEmpty; + } + + dematerializeRecord() { + this._isDematerializing = true; + + // TODO IGOR add a test that fails when this is missing, something that involves canceling a destroy + // and the destroy not happening, and then later on trying to destroy + this._doNotDestroy = false; + // this has to occur before the internal model is removed + // for legacy compat. + const { identifier } = this; + this.store._instanceCache.removeRecord(identifier); + + // move to an empty never-loaded state + // ensure any record notifications happen prior to us + // unseting the record but after we've triggered + // destroy + this.store._backburner.join(() => { + this._recordData.unloadRecord(); + }); + + this.hasRecord = false; // this must occur after relationship removal + this.error = null; + this.store.recordArrayManager.recordDidChange(this.identifier); + } + + deleteRecord() { + run(() => { + const backburner = this.store._backburner; + backburner.run(() => { + if (this._recordData.setIsDeleted) { + this._recordData.setIsDeleted(true); + } + + if (this.isNew()) { + // destroyRecord follows up deleteRecord with save(). This prevents an unecessary save for a new record + this._deletedRecordWasNew = true; + this.unloadRecord(); + } + }); + }); + } + + /* + Unload the record for this internal model. This will cause the record to be + destroyed and freed up for garbage collection. It will also do a check + for cleaning up internal models. + + This check is performed by first computing the set of related internal + models. If all records in this set are unloaded, then the entire set is + destroyed. Otherwise, nothing in the set is destroyed. + + This means that this internal model will be freed up for garbage collection + once all models that refer to it via some relationship are also unloaded. + */ + unloadRecord() { + if (this.isDestroyed) { + return; + } + if (DEBUG) { + const requests = this.store.getRequestStateService().getPendingRequestsForRecord(this.identifier); + if ( + requests.some((req) => { + return req.type === 'mutation'; + }) + ) { + assert('You can only unload a record which is not inFlight. `' + this + '`'); + } + } + this.dematerializeRecord(); + if (this._scheduledDestroy === null) { + this._scheduledDestroy = emberBackburner.schedule('destroy', this, '_checkForOrphanedInternalModels'); + } + } + + hasScheduledDestroy() { + return !!this._scheduledDestroy; + } + + cancelDestroy() { + assert( + `You cannot cancel the destruction of an InternalModel once it has already been destroyed`, + !this.isDestroyed + ); + + this._doNotDestroy = true; + this._isDematerializing = false; + cancel(this._scheduledDestroy); + this._scheduledDestroy = null; + } + + // typically, we prefer to async destroy this lets us batch cleanup work. + // Unfortunately, some scenarios where that is not possible. Such as: + // + // ```js + // const record = store.findRecord(‘record’, 1); + // record.unloadRecord(); + // store.createRecord(‘record’, 1); + // ``` + // + // In those scenarios, we make that model's cleanup work, sync. + // + destroySync() { + if (this._isDematerializing) { + this.cancelDestroy(); + } + this._checkForOrphanedInternalModels(); + if (this.isDestroyed || this.isDestroying) { + return; + } + + // just in-case we are not one of the orphaned, we should still + // still destroy ourselves + this.destroy(); + } + + _checkForOrphanedInternalModels() { + this._isDematerializing = false; + this._scheduledDestroy = null; + if (this.isDestroyed) { + return; + } + } + + destroyFromRecordData() { + if (this._doNotDestroy) { + this._doNotDestroy = false; + return; + } + this.destroy(); + } + + destroy() { + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + assert( + 'Cannot destroy an internalModel while its record is materialized', + !record || record.isDestroyed || record.isDestroying + ); + this.isDestroying = true; + + internalModelFactoryFor(this.store).remove(this); + this._isDestroyed = true; + } + + setupData(data) { + if (this.isNew()) { + this.store._notificationManager.notify(this.identifier, 'identity'); + } + this._recordData.pushData(data, this.hasRecord); + } + + notifyAttributes(keys: string[]): void { + if (this.hasRecord) { + let manager = this.store._notificationManager; + let { identifier } = this; + + if (!keys || !keys.length) { + manager.notify(identifier, 'attributes'); + } else { + for (let i = 0; i < keys.length; i++) { + manager.notify(identifier, 'attributes', keys[i]); + } + } + } + } + + get isDestroyed(): boolean { + return this._isDestroyed; + } + + hasChangedAttributes(): boolean { + if (!this.hasRecordData) { + // no need to calculate changed attributes when calling `findRecord` + return false; + } + return this._recordData.hasChangedAttributes(); + } + + changedAttributes(): ChangedAttributesHash { + if (!this.hasRecordData) { + // no need to calculate changed attributes when calling `findRecord` + return {}; + } + return this._recordData.changedAttributes(); + } + + adapterWillCommit(): void { + this._recordData.willCommit(); + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + if (record && isDSModel(record)) { + record.errors.clear(); + } + } + + notifyHasManyChange(key: string) { + if (this.hasRecord) { + this.store._notificationManager.notify(this.identifier, 'relationships', key); + } + } + + notifyBelongsToChange(key: string) { + if (this.hasRecord) { + this.store._notificationManager.notify(this.identifier, 'relationships', key); + } + } + + notifyPropertyChange(key: string) { + if (this.hasRecord) { + // TODO this should likely *mostly* be the `attributes` bucket + // but it seems for local mutations we rely on computed updating + // iteself when set. As we design our own thing we may need to change + // that. + this.store._notificationManager.notify(this.identifier, 'property', key); + } + } + + notifyStateChange(key?: string) { + if (this.hasRecord) { + this.store._notificationManager.notify(this.identifier, 'state'); + } + if (!key || key === 'isDeletionCommitted') { + this.store.recordArrayManager.recordDidChange(this.identifier); + } + } + + rollbackAttributes() { + this.store._backburner.join(() => { + let dirtyKeys = this._recordData.rollbackAttributes(); + + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + if (record && isDSModel(record)) { + record.errors.clear(); + } + + if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) { + this.notifyAttributes(dirtyKeys); + } + }); + } + + removeFromInverseRelationships() { + if (this.hasRecordData) { + this.store._backburner.join(() => { + this._recordData.removeFromInverseRelationships(); + }); + } + } + + /* + When a find request is triggered on the store, the user can optionally pass in + attributes and relationships to be preloaded. These are meant to behave as if they + came back from the server, except the user obtained them out of band and is informing + the store of their existence. The most common use case is for supporting client side + nested URLs, such as `/posts/1/comments/2` so the user can do + `store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post. + + Preloaded data can be attributes and relationships passed in either as IDs or as actual + models. + */ + preloadData(preload) { + let jsonPayload: JsonApiResource = {}; + //TODO(Igor) consider the polymorphic case + Object.keys(preload).forEach((key) => { + let preloadValue = preload[key]; + let relationshipMeta = this.modelClass.metaForProperty(key); + if (relationshipMeta.isRelationship) { + if (!jsonPayload.relationships) { + jsonPayload.relationships = {}; + } + jsonPayload.relationships[key] = this._preloadRelationship(key, preloadValue); + } else { + if (!jsonPayload.attributes) { + jsonPayload.attributes = {}; + } + jsonPayload.attributes[key] = preloadValue; + } + }); + this._recordData.pushData(jsonPayload); + } + + _preloadRelationship(key, preloadValue) { + let relationshipMeta = this.modelClass.metaForProperty(key); + let modelClass = relationshipMeta.type; + let data; + if (relationshipMeta.kind === 'hasMany') { + assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue)); + data = preloadValue.map((value) => this._convertPreloadRelationshipToJSON(value, modelClass)); + } else { + data = this._convertPreloadRelationshipToJSON(preloadValue, modelClass); + } + return { data }; + } + + _convertPreloadRelationshipToJSON(value, modelClass) { + if (typeof value === 'string' || typeof value === 'number') { + return { type: modelClass, id: value }; + } + let internalModel; + if (value._internalModel) { + internalModel = value._internalModel; + } else { + internalModel = value; + } + // TODO IGOR DAVID assert if no id is present + return { type: internalModel.modelName, id: internalModel.id }; + } + + /* + * calling `InstanceCache.setRecordId` is necessary to update + * the cache index for this record if we have changed. + * + * However, since the store is not aware of whether the update + * is from us (via user set) or from a push of new data + * it will also call us so that we can notify and update state. + * + * When it does so it calls with `fromCache` so that we can + * short-circuit instead of cycling back. + * + * This differs from the short-circuit in the `_isUpdatingId` + * case in that the the cache can originate the call to setId, + * so on first entry we will still need to do our own update. + */ + setId(id: string | null, fromCache: boolean = false) { + if (this._isUpdatingId === true) { + return; + } + this._isUpdatingId = true; + let didChange = id !== this._id; + this._id = id; + + if (didChange && id !== null) { + if (!fromCache) { + this.store._instanceCache.setRecordId(this.modelName, id, this.clientId); + } + // internal set of ID to get it to RecordData from DS.Model + // if we are within create we may not have a recordData yet. + if (this.hasRecordData && this._recordData.__setId) { + this._recordData.__setId(id); + } + } + + if (didChange && this.hasRecord) { + this.store._notificationManager.notify(this.identifier, 'identity'); + } + this._isUpdatingId = false; + } + + didError() {} + + /* + If the adapter did not return a hash in response to a commit, + merge the changed attributes and relationships into the existing + saved data. + */ + adapterDidCommit(data) { + this._recordData.didCommit(data); + this.store.recordArrayManager.recordDidChange(this.identifier); + } + + hasErrors(): boolean { + // TODO add assertion forcing consuming RecordData's to implement getErrors + if (this._recordData.getErrors) { + return this._recordData.getErrors(this.identifier).length > 0; + } else { + let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); + // we can't have errors if we never tried loading + if (!record) { + return false; + } + let errors = (record as DSModel).errors; + return errors.length > 0; + } + } + + // FOR USE DURING COMMIT PROCESS + adapterDidInvalidate(parsedErrors, error?) { + // TODO @runspired this should be handled by RecordState + // and errors should be dirtied but lazily fetch if at + // all possible. We should only notify errors here. + let attribute; + if (error && parsedErrors) { + // TODO add assertion forcing consuming RecordData's to implement getErrors + if (!this._recordData.getErrors) { + let record = this.store._instanceCache.getRecord(this.identifier) as DSModel; + let errors = record.errors; + for (attribute in parsedErrors) { + if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) { + errors.add(attribute, parsedErrors[attribute]); + } + } + } + + let jsonApiErrors: JsonApiValidationError[] = errorsHashToArray(parsedErrors); + if (jsonApiErrors.length === 0) { + jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }]; + } + this._recordData.commitWasRejected(this.identifier, jsonApiErrors); + } else { + this._recordData.commitWasRejected(this.identifier); + } + } + + notifyErrorsChange() { + this.store._notificationManager.notify(this.identifier, 'errors'); + } + + adapterDidError() { + this._recordData.commitWasRejected(); + } + + toString() { + return `<${this.modelName}:${this.id}>`; + } +} diff --git a/packages/store/addon/-private/system/references/record.ts b/packages/store/addon/-private/model/record-reference.ts similarity index 83% rename from packages/store/addon/-private/system/references/record.ts rename to packages/store/addon/-private/model/record-reference.ts index 9e0b8557877..722c6e41580 100644 --- a/packages/store/addon/-private/system/references/record.ts +++ b/packages/store/addon/-private/model/record-reference.ts @@ -1,14 +1,19 @@ -import { dependentKeyCompat } from '@ember/object/compat'; -import { cached, tracked } from '@glimmer/tracking'; +import { assert } from '@ember/debug'; +import { tracked } from '@glimmer/tracking'; +/** + @module @ember-data/store +*/ import RSVP, { resolve } from 'rsvp'; -import type { SingleResourceDocument } from '../../ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import type CoreStore from '../core-store'; -import { NotificationType, unsubscribe } from '../record-notification-manager'; -import Reference from './reference'; +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 Store from '../core-store'; +import type { NotificationType } from '../record-notification-manager'; +import { unsubscribe } from '../record-notification-manager'; + /** @module @ember-data/store */ @@ -21,15 +26,14 @@ import Reference from './reference'; @public @extends Reference */ -export default class RecordReference extends Reference { +export default class RecordReference { // unsubscribe token given to us by the notification manager #token!: Object; - #identifier; + #identifier: StableRecordIdentifier; @tracked _ref = 0; - constructor(public store: CoreStore, identifier: StableRecordIdentifier) { - super(store, identifier); + constructor(public store: Store, identifier: StableRecordIdentifier) { this.#identifier = identifier; this.#token = store._notificationManager.subscribe( identifier, @@ -45,22 +49,10 @@ export default class RecordReference extends Reference { unsubscribe(this.#token); } - public get type(): string { + get type(): string { return this.identifier().type; } - @cached - @dependentKeyCompat - private get _id(): string | null { - this._ref; // consume the tracked prop - let identifier = this.identifier(); - if (identifier) { - return identifier.id; - } - - return null; - } - /** The `id` of the record that this reference refers to. @@ -80,7 +72,8 @@ export default class RecordReference extends Reference { @return {String} The id of the record. */ id() { - return this._id; + this._ref; // consume the tracked prop + return this.#identifier.id; } /** @@ -166,6 +159,7 @@ export default class RecordReference extends Reference { @return a promise for the value (record or relationship) */ push(objectOrPromise: SingleResourceDocument | Promise): RSVP.Promise { + // TODO @deprecate pushing unresolved payloads return resolve(objectOrPromise).then((data) => { return this.store.push(data); }); @@ -214,7 +208,7 @@ export default class RecordReference extends Reference { if (id !== null) { return this.store.findRecord(this.type, id); } - throw new Error(`Unable to fetch record of type ${this.type} without an id`); + assert(`Unable to fetch record of type ${this.type} without an id`); } /** @@ -239,6 +233,6 @@ export default class RecordReference extends Reference { if (id !== null) { return this.store.findRecord(this.type, id, { reload: true }); } - throw new Error(`Unable to fetch record of type ${this.type} without an id`); + assert(`Unable to fetch record of type ${this.type} without an id`); } } diff --git a/packages/store/addon/-private/system/model/shim-model-class.ts b/packages/store/addon/-private/model/shim-model-class.ts similarity index 61% rename from packages/store/addon/-private/system/model/shim-model-class.ts rename to packages/store/addon/-private/model/shim-model-class.ts index 790b91bd71d..475871c107f 100644 --- a/packages/store/addon/-private/system/model/shim-model-class.ts +++ b/packages/store/addon/-private/model/shim-model-class.ts @@ -1,16 +1,17 @@ import { DEBUG } from '@glimmer/env'; -import { ModelSchema } from '../../ts-interfaces/ds-model'; -import type { AttributeSchema, RelationshipSchema } from '../../ts-interfaces/record-data-schemas'; -import type { Dict } from '../../ts-interfaces/utils'; -import type CoreStore from '../core-store'; +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 type Store from '../core-store'; import WeakCache from '../weak-cache'; -const AvailableShims = new WeakCache>(DEBUG ? 'schema-shims' : ''); +const AvailableShims = new WeakCache>(DEBUG ? 'schema-shims' : ''); AvailableShims._generator = () => { return Object.create(null) as Dict; }; -export function getShimClass(store: CoreStore, modelName: string): ShimModelClass { +export function getShimClass(store: Store, modelName: string): ShimModelClass { let shims = AvailableShims.lookup(store); let shim = shims[modelName]; if (shim === undefined) { @@ -33,11 +34,11 @@ function mapFromHash(hash: Dict): Map { // Mimics the static apis of DSModel export default class ShimModelClass implements ModelSchema { // TODO Maybe expose the class here? - constructor(private __store: CoreStore, public modelName: string) {} + constructor(private __store: Store, public modelName: string) {} get fields(): Map { - let attrs = this.__store._attributesDefinitionFor({ type: this.modelName }); - let relationships = this.__store._relationshipsDefinitionFor({ type: this.modelName }); + 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)); @@ -45,17 +46,17 @@ export default class ShimModelClass implements ModelSchema { } get attributes(): Map { - let attrs = this.__store._attributesDefinitionFor({ type: this.modelName }); + let attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); return mapFromHash(attrs); } get relationshipsByName(): Map { - let relationships = this.__store._relationshipsDefinitionFor({ type: this.modelName }); + let relationships = this.__store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName }); return mapFromHash(relationships); } eachAttribute(callback: (this: T | undefined, key: string, attribute: AttributeSchema) => void, binding?: T) { - let attrDefs = this.__store._attributesDefinitionFor({ type: this.modelName }); + let attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); Object.keys(attrDefs).forEach((key) => { callback.call(binding, key, attrDefs[key] as AttributeSchema); }); @@ -65,7 +66,9 @@ export default class ShimModelClass implements ModelSchema { callback: (this: T | undefined, key: string, relationship: RelationshipSchema) => void, binding?: T ) { - let relationshipDefs = this.__store._relationshipsDefinitionFor({ type: this.modelName }); + let relationshipDefs = this.__store + .getSchemaDefinitionService() + .relationshipsDefinitionFor({ type: this.modelName }); Object.keys(relationshipDefs).forEach((key) => { callback.call(binding, key, relationshipDefs[key] as RelationshipSchema); }); @@ -75,7 +78,9 @@ export default class ShimModelClass implements ModelSchema { callback: (this: T | undefined, key: string, relationship: RelationshipSchema) => void, binding?: T ) { - let relationshipDefs = this.__store._relationshipsDefinitionFor({ type: this.modelName }); + let relationshipDefs = this.__store + .getSchemaDefinitionService() + .relationshipsDefinitionFor({ type: this.modelName }); Object.keys(relationshipDefs).forEach((key) => { if (relationshipDefs[key]!.type) { callback.call(binding, key, relationshipDefs[key] as RelationshipSchema); diff --git a/packages/store/addon/-private/system/normalize-model-name.ts b/packages/store/addon/-private/normalize-model-name.ts similarity index 100% rename from packages/store/addon/-private/system/normalize-model-name.ts rename to packages/store/addon/-private/normalize-model-name.ts diff --git a/packages/store/addon/-private/system/promise-proxies.ts b/packages/store/addon/-private/promise-proxies.ts similarity index 98% rename from packages/store/addon/-private/system/promise-proxies.ts rename to packages/store/addon/-private/promise-proxies.ts index b5cfb5120ec..edc5ebd49d7 100644 --- a/packages/store/addon/-private/system/promise-proxies.ts +++ b/packages/store/addon/-private/promise-proxies.ts @@ -4,7 +4,8 @@ import { reads } from '@ember/object/computed'; import { resolve } from 'rsvp'; -import type { Dict } from '../ts-interfaces/utils'; +import type { Dict } from '@ember-data/types/q/utils'; + import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; /** diff --git a/packages/store/addon/-private/system/promise-proxy-base.d.ts b/packages/store/addon/-private/promise-proxy-base.d.ts similarity index 100% rename from packages/store/addon/-private/system/promise-proxy-base.d.ts rename to packages/store/addon/-private/promise-proxy-base.d.ts diff --git a/packages/store/addon/-private/system/promise-proxy-base.js b/packages/store/addon/-private/promise-proxy-base.js similarity index 100% rename from packages/store/addon/-private/system/promise-proxy-base.js rename to packages/store/addon/-private/promise-proxy-base.js diff --git a/packages/store/addon/-private/system/record-array-manager.ts b/packages/store/addon/-private/record-array-manager.ts similarity index 92% rename from packages/store/addon/-private/system/record-array-manager.ts rename to packages/store/addon/-private/record-array-manager.ts index d43469a6e66..44f01088ecc 100644 --- a/packages/store/addon/-private/system/record-array-manager.ts +++ b/packages/store/addon/-private/record-array-manager.ts @@ -9,12 +9,14 @@ import { _backburner as emberBackburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; // import isStableIdentifier from '../identifiers/is-stable-identifier'; -import type { CollectionResourceDocument, Meta } from '../ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type { Dict } from '../ts-interfaces/utils'; -import type CoreStore from './core-store'; -import { AdapterPopulatedRecordArray, RecordArray } from './record-arrays'; -import { internalModelFactoryFor } from './store/internal-model-factory'; +import type { CollectionResourceDocument, Meta } 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 type Store from './core-store'; +import { internalModelFactoryFor } from './internal-model-factory'; +import AdapterPopulatedRecordArray from './record-arrays/adapter-populated-record-array'; +import RecordArray from './record-arrays/record-array'; import WeakCache from './weak-cache'; const RecordArraysCache = new WeakCache>(DEBUG ? 'record-arrays' : ''); @@ -36,7 +38,7 @@ function getIdentifier(identifier: StableRecordIdentifier): StableRecordIdentifi return identifier; } -function shouldIncludeInRecordArrays(store: CoreStore, identifier: StableRecordIdentifier): boolean { +function shouldIncludeInRecordArrays(store: Store, identifier: StableRecordIdentifier): boolean { const cache = internalModelFactoryFor(store); const internalModel = cache.peek(identifier); @@ -51,14 +53,14 @@ function shouldIncludeInRecordArrays(store: CoreStore, identifier: StableRecordI @internal */ class RecordArrayManager { - declare store: CoreStore; + declare store: Store; declare isDestroying: boolean; declare isDestroyed: boolean; declare _liveRecordArrays: Dict; declare _pendingIdentifiers: Dict; declare _adapterPopulatedRecordArrays: RecordArray[]; - constructor(options: { store: CoreStore }) { + constructor(options: { store: Store }) { this.store = options.store; this.isDestroying = false; this.isDestroyed = false; @@ -278,8 +280,8 @@ class RecordArrayManager { // TODO this assign kills the root reference but a deep-copy would be required // for both meta and links to actually not be by-ref. We whould likely change // this to a dev-only deep-freeze. - meta: Object.assign({} as Meta, payload!.meta), - links: Object.assign({}, payload!.links), + meta: Object.assign({} as Meta, payload?.meta), + links: Object.assign({}, payload?.links), }); this._associateWithRecordArray(identifiers, array); @@ -392,11 +394,7 @@ function removeFromArray(array: RecordArray[], item: RecordArray): boolean { return false; } -function updateLiveRecordArray( - store: CoreStore, - recordArray: RecordArray, - identifiers: StableRecordIdentifier[] -): void { +function updateLiveRecordArray(store: Store, recordArray: RecordArray, identifiers: StableRecordIdentifier[]): void { let identifiersToAdd: StableRecordIdentifier[] = []; let identifiersToRemove: StableRecordIdentifier[] = []; @@ -426,13 +424,13 @@ function updateLiveRecordArray( } } -function removeFromAdapterPopulatedRecordArrays(store: CoreStore, identifiers: StableRecordIdentifier[]): void { +function removeFromAdapterPopulatedRecordArrays(store: Store, identifiers: StableRecordIdentifier[]): void { for (let i = 0; i < identifiers.length; i++) { removeFromAll(store, identifiers[i]); } } -function removeFromAll(store: CoreStore, identifier: StableRecordIdentifier): void { +function removeFromAll(store: Store, identifier: StableRecordIdentifier): void { identifier = getIdentifier(identifier); const recordArrays = recordArraysForIdentifier(identifier); diff --git a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts b/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts similarity index 87% rename from packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts rename to packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts index cdbeba1a17e..39fd15b0bca 100644 --- a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts +++ b/packages/store/addon/-private/record-arrays/adapter-populated-record-array.ts @@ -1,21 +1,22 @@ import type NativeArray from '@ember/array/-private/native-array'; import { assert } from '@ember/debug'; -import type { PromiseArray, RecordArrayManager } from 'ember-data/-private'; - -import type { CollectionResourceDocument, Links, Meta, PaginationLinks } from '../../ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import type { FindOptions } from '../../ts-interfaces/store'; -import type { Dict } from '../../ts-interfaces/utils'; -import type CoreStore from '../core-store'; +import type { CollectionResourceDocument, Links, Meta, 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 type Store from '../core-store'; +import type { PromiseArray } from '../promise-proxies'; import { promiseArray } from '../promise-proxies'; +import type RecordArrayManager from '../record-array-manager'; import SnapshotRecordArray from '../snapshot-record-array'; import RecordArray from './record-array'; export interface AdapterPopulatedRecordArrayCreateArgs { modelName: string; - store: CoreStore; + store: Store; manager: RecordArrayManager; content: NativeArray; isLoaded: boolean; @@ -89,7 +90,7 @@ export default class AdapterPopulatedRecordArray extends RecordArray { const { store, query } = this; // TODO save options from initial request? - return promiseArray(store._query(this.modelName, query, this, {})); + return promiseArray(store.query(this.modelName, query, { _recordArray: this })); } _setObjects(identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument) { diff --git a/packages/store/addon/-private/system/record-arrays/record-array.ts b/packages/store/addon/-private/record-arrays/record-array.ts similarity index 95% rename from packages/store/addon/-private/system/record-arrays/record-array.ts rename to packages/store/addon/-private/record-arrays/record-array.ts index cc1bd439fd1..b477d07e686 100644 --- a/packages/store/addon/-private/system/record-arrays/record-array.ts +++ b/packages/store/addon/-private/record-arrays/record-array.ts @@ -12,22 +12,22 @@ import { Promise } from 'rsvp'; import type { RecordArrayManager, Snapshot } from 'ember-data/-private'; import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; +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 { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import type { FindOptions } from '../../ts-interfaces/store'; -import type CoreStore from '../core-store'; +import type Store from '../core-store'; import type { PromiseArray } from '../promise-proxies'; import { promiseArray } from '../promise-proxies'; import SnapshotRecordArray from '../snapshot-record-array'; -function recordForIdentifier(store: CoreStore, identifier: StableRecordIdentifier): RecordInstance { +function recordForIdentifier(store: Store, identifier: StableRecordIdentifier): RecordInstance { return store._instanceCache.getRecord(identifier); } export interface RecordArrayCreateArgs { modelName: string; - store: CoreStore; + store: Store; manager: RecordArrayManager; content: NativeArray; isLoaded: boolean; @@ -82,7 +82,7 @@ export default class RecordArray extends ArrayProxy | null; declare manager: RecordArrayManager; diff --git a/packages/store/addon/-private/record-data-for.ts b/packages/store/addon/-private/record-data-for.ts new file mode 100644 index 00000000000..3a93d5495d6 --- /dev/null +++ b/packages/store/addon/-private/record-data-for.ts @@ -0,0 +1,39 @@ +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; + +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; + +import WeakCache from './weak-cache'; + +/* + * Returns the RecordData instance associated with a given + * Model or Identifier + */ + +const RecordDataForIdentifierCache = new WeakCache( + DEBUG ? 'recordData' : '' +); + +export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: RecordData): void { + assert( + `Illegal set of identifier`, + !RecordDataForIdentifierCache.has(identifier) || RecordDataForIdentifierCache.get(identifier) === recordData + ); + RecordDataForIdentifierCache.set(identifier, recordData); +} + +export function removeRecordDataFor(identifier: StableRecordIdentifier): void { + RecordDataForIdentifierCache.delete(identifier); +} + +export default function recordDataFor(instance: StableRecordIdentifier): RecordData | null; +export default function recordDataFor(instance: RecordInstance): RecordData; +export default function recordDataFor(instance: StableRecordIdentifier | RecordInstance): RecordData | null { + if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { + return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as RecordData; + } + + return null; +} diff --git a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts b/packages/store/addon/-private/record-data-store-wrapper.ts similarity index 90% rename from packages/store/addon/-private/system/store/record-data-store-wrapper.ts rename to packages/store/addon/-private/record-data-store-wrapper.ts index f28b98aa210..8efca4589c3 100644 --- a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts +++ b/packages/store/addon/-private/record-data-store-wrapper.ts @@ -1,20 +1,20 @@ import { importSync } from '@embroider/macros'; -import type { RelationshipDefinition } from '@ember-data/model/-private/system/relationships/relationship-meta'; +import type { RelationshipDefinition } from '@ember-data/model/-private/relationship-meta'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; - -import type { IdentifierCache } from '../../identifiers/cache'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordData } from '../../ts-interfaces/record-data'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordData } from '@ember-data/types/q/record-data'; import type { AttributesSchema, RelationshipSchema, RelationshipsSchema, -} from '../../ts-interfaces/record-data-schemas'; -import type { RecordDataStoreWrapper as StoreWrapper } from '../../ts-interfaces/record-data-store-wrapper'; -import constructResource from '../../utils/construct-resource'; -import type CoreStore from '../core-store'; +} from '@ember-data/types/q/record-data-schemas'; +import type { RecordDataStoreWrapper as StoreWrapper } from '@ember-data/types/q/record-data-store-wrapper'; + +import type Store from './core-store'; +import type { IdentifierCache } from './identifier-cache'; import { internalModelFactoryFor } from './internal-model-factory'; +import constructResource from './utils/construct-resource'; /** @module @ember-data/store @@ -38,9 +38,9 @@ if (HAS_RECORD_DATA_PACKAGE) { export default class RecordDataStoreWrapper implements StoreWrapper { declare _willNotify: boolean; declare _pendingNotifies: Map>; - declare _store: CoreStore; + declare _store: Store; - constructor(_store: CoreStore) { + constructor(_store: Store) { this._store = _store; this._willNotify = false; this._pendingNotifies = new Map(); @@ -107,11 +107,11 @@ export default class RecordDataStoreWrapper implements StoreWrapper { } attributesDefinitionFor(type: string): AttributesSchema { - return this._store._attributesDefinitionFor({ type }); + return this._store.getSchemaDefinitionService().attributesDefinitionFor({ type }); } relationshipsDefinitionFor(type: string): RelationshipsSchema { - return this._store._relationshipsDefinitionFor({ type }); + return this._store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); } inverseForRelationship(type: string, key: string): string | null { @@ -206,11 +206,11 @@ export default class RecordDataStoreWrapper implements StoreWrapper { identifier = this.identifierCache.getOrCreateRecordIdentifier(resource); } - return this._store.recordDataFor(identifier, isCreate); + return this._store._instanceCache.recordDataFor(identifier, isCreate); } setRecordId(type: string, id: string, lid: string) { - this._store.setRecordId(type, id, lid); + this._store._instanceCache.setRecordId(type, id, lid); } isRecordInUse(type: string, id: string | null, lid: string): boolean; diff --git a/packages/store/addon/-private/system/record-notification-manager.ts b/packages/store/addon/-private/record-notification-manager.ts similarity index 90% rename from packages/store/addon/-private/system/record-notification-manager.ts rename to packages/store/addon/-private/record-notification-manager.ts index bfcb5af8753..e555d2247f1 100644 --- a/packages/store/addon/-private/system/record-notification-manager.ts +++ b/packages/store/addon/-private/record-notification-manager.ts @@ -1,7 +1,8 @@ import { DEBUG } from '@glimmer/env'; -import type { RecordIdentifier, StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type CoreStore from './core-store'; +import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +import type Store from './core-store'; import WeakCache from './weak-cache'; type UnsubscribeToken = Object; @@ -41,7 +42,7 @@ export function unsubscribe(token: UnsubscribeToken) { Currently only support a single callback per identifier */ export default class NotificationManager { - constructor(private store: CoreStore) {} + constructor(private store: Store) {} subscribe(identifier: RecordIdentifier, callback: NotificationCallback): UnsubscribeToken { let stableIdentifier = this.store.identifierCache.getOrCreateRecordIdentifier(identifier); @@ -52,6 +53,10 @@ export default class NotificationManager { return unsubToken; } + unsubscribe(token: UnsubscribeToken) { + unsubscribe(token); + } + notify(identifier: RecordIdentifier, value: 'attributes' | 'relationships' | 'property', key?: string): boolean; notify(identifier: RecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'unload' | 'state'): boolean; notify(identifier: RecordIdentifier, value: NotificationType, key?: string): boolean { diff --git a/packages/store/addon/-private/system/request-cache.ts b/packages/store/addon/-private/request-cache.ts similarity index 92% rename from packages/store/addon/-private/system/request-cache.ts rename to packages/store/addon/-private/request-cache.ts index d2f78be6f6d..30b55854b07 100644 --- a/packages/store/addon/-private/system/request-cache.ts +++ b/packages/store/addon/-private/request-cache.ts @@ -4,9 +4,8 @@ import type { Request, RequestState, SaveRecordMutation, -} from '../ts-interfaces/fetch-manager'; -import { RequestStateEnum } from '../ts-interfaces/fetch-manager'; -import type { RecordIdentifier } from '../ts-interfaces/identifier'; +} from '@ember-data/types/q/fetch-manager'; +import type { RecordIdentifier } from '@ember-data/types/q/identifier'; const Touching: unique symbol = Symbol('touching'); export const RequestPromise: unique symbol = Symbol('promise'); @@ -36,7 +35,7 @@ export default class RequestCache { this._pending[lid] = []; } let request: InternalRequest = { - state: RequestStateEnum.pending, + state: 'pending', request: queryRequest, type, } as InternalRequest; @@ -48,7 +47,7 @@ export default class RequestCache { (result) => { this._dequeue(lid, request); let finalizedRequest = { - state: RequestStateEnum.fulfilled, + state: 'fulfilled', request: queryRequest, type, response: { data: result }, @@ -60,7 +59,7 @@ export default class RequestCache { (error) => { this._dequeue(lid, request); let finalizedRequest = { - state: RequestStateEnum.rejected, + state: 'rejected', request: queryRequest, type, response: { data: error && error.error }, diff --git a/packages/store/addon/-private/system/schema-definition-service.ts b/packages/store/addon/-private/schema-definition-service.ts similarity index 87% rename from packages/store/addon/-private/system/schema-definition-service.ts rename to packages/store/addon/-private/schema-definition-service.ts index 9c586d7621b..41add669a96 100644 --- a/packages/store/addon/-private/system/schema-definition-service.ts +++ b/packages/store/addon/-private/schema-definition-service.ts @@ -7,9 +7,9 @@ import { importSync } from '@embroider/macros'; import type Model from '@ember-data/model'; import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_STRING_ARG_SCHEMAS } from '@ember-data/private-build-infra/deprecations'; +import type { RecordIdentifier } from '@ember-data/types/q/identifier'; +import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; -import type { RecordIdentifier } from '../ts-interfaces/identifier'; -import type { AttributesSchema, RelationshipsSchema } from '../ts-interfaces/record-data-schemas'; import type Store from './core-store'; import normalizeModelName from './normalize-model-name'; @@ -27,7 +27,6 @@ if (HAS_MODEL_PACKAGE) { } export class DSModelSchemaDefinitionService { - private _modelFactoryCache = Object.create(null); private _relationshipsDefCache = Object.create(null); private _attributesDefCache = Object.create(null); @@ -98,7 +97,7 @@ export class DSModelSchemaDefinitionService { doesTypeExist(modelName: string): boolean { let normalizedModelName = normalizeModelName(modelName); - let factory = getModelFactory(this.store, this._modelFactoryCache, normalizedModelName); + let factory = getModelFactory(this.store, this.store._modelFactoryCache, normalizedModelName); return factory !== null; } @@ -108,7 +107,8 @@ export function getModelFactory(store: Store, cache, normalizedModelName: string let factory = cache[normalizedModelName]; if (!factory) { - factory = _lookupModelFactory(store, normalizedModelName); + let owner: any = getOwner(store); + factory = owner.factoryFor(`model:${normalizedModelName}`); if (!factory && HAS_MODEL_PACKAGE) { //Support looking up mixins as base types for polymorphic relationships @@ -134,9 +134,3 @@ export function getModelFactory(store: Store, cache, normalizedModelName: string return factory; } - -export function _lookupModelFactory(store: Store, normalizedModelName: string): Model | null { - let owner: any = getOwner(store); - - return owner.factoryFor(`model:${normalizedModelName}`); -} diff --git a/packages/store/addon/-private/system/store/serializer-response.ts b/packages/store/addon/-private/serializer-response.ts similarity index 85% rename from packages/store/addon/-private/system/store/serializer-response.ts rename to packages/store/addon/-private/serializer-response.ts index 74361887fbe..c95b2015d93 100644 --- a/packages/store/addon/-private/system/store/serializer-response.ts +++ b/packages/store/addon/-private/serializer-response.ts @@ -1,11 +1,12 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { JsonApiDocument } from '../../ts-interfaces/ember-data-json-api'; -import { AdapterPayload } from '../../ts-interfaces/minimum-adapter-interface'; -import { MinimumSerializerInterface, RequestType } from '../../ts-interfaces/minimum-serializer-interface'; -import CoreStore from '../core-store'; -import ShimModelClass from '../model/shim-model-class'; +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 Store from './core-store'; +import type ShimModelClass from './model/shim-model-class'; /** This is a helper method that validates a JSON API top-level document @@ -69,7 +70,7 @@ function validateDocumentStructure(doc?: AdapterPayload | JsonApiDocument): asse export function normalizeResponseHelper( serializer: MinimumSerializerInterface | null, - store: CoreStore, + store: Store, modelClass: ShimModelClass, payload: AdapterPayload, id: string | null, diff --git a/packages/store/addon/-private/system/snapshot-record-array.ts b/packages/store/addon/-private/snapshot-record-array.ts similarity index 96% rename from packages/store/addon/-private/system/snapshot-record-array.ts rename to packages/store/addon/-private/snapshot-record-array.ts index f18ecfdbbb5..2f48b463f83 100644 --- a/packages/store/addon/-private/system/snapshot-record-array.ts +++ b/packages/store/addon/-private/snapshot-record-array.ts @@ -5,10 +5,10 @@ import { deprecate } from '@ember/debug'; import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; +import type { ModelSchema } from '@ember-data/types/q/ds-model'; +import type { FindOptions } from '@ember-data/types/q/store'; +import type { Dict } from '@ember-data/types/q/utils'; -import type { ModelSchema } from '../ts-interfaces/ds-model'; -import { FindOptions } from '../ts-interfaces/store'; -import type { Dict } from '../ts-interfaces/utils'; import type RecordArray from './record-arrays/record-array'; import type Snapshot from './snapshot'; /** diff --git a/packages/store/addon/-private/system/snapshot.ts b/packages/store/addon/-private/snapshot.ts similarity index 90% rename from packages/store/addon/-private/system/snapshot.ts rename to packages/store/addon/-private/snapshot.ts index 5ebbfb3adac..67813e09b86 100644 --- a/packages/store/addon/-private/system/snapshot.ts +++ b/packages/store/addon/-private/snapshot.ts @@ -10,19 +10,19 @@ import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@ember-data/private-build-infra/deprecations'; import type BelongsToRelationship from '@ember-data/record-data/addon/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/record-data/addon/-private/relationships/state/has-many'; +import type { DSModel, DSModelSchema, ModelSchema } from '@ember-data/types/q/ds-model'; import type { ExistingResourceIdentifierObject, NewResourceIdentifierObject, -} from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; - -import type { DSModel, DSModelSchema, ModelSchema } from '../ts-interfaces/ds-model'; -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import { OptionsHash } from '../ts-interfaces/minimum-serializer-interface'; -import type { ChangedAttributesHash } from '../ts-interfaces/record-data'; -import type { AttributeSchema, RelationshipSchema } from '../ts-interfaces/record-data-schemas'; -import type { RecordInstance } from '../ts-interfaces/record-instance'; -import type { FindOptions } from '../ts-interfaces/store'; -import type { Dict } from '../ts-interfaces/utils'; +} from '@ember-data/types/q/ember-data-json-api'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { OptionsHash } from '@ember-data/types/q/minimum-serializer-interface'; +import type { ChangedAttributesHash } from '@ember-data/types/q/record-data'; +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 Store from './core-store'; import type InternalModel from './model/internal-model'; @@ -32,12 +32,6 @@ function schemaIsDSModel(schema: ModelSchema | DSModelSchema): schema is DSModel return (schema as DSModelSchema).isModel === true; } -type ProtoExntends = U & Omit; -interface _PrivateSnapshot { - _internalModel: InternalModel; -} -export type PrivateSnapshot = ProtoExntends; - /** Snapshot is not directly instantiable. Instances are provided to a consuming application's @@ -70,7 +64,7 @@ export default class Snapshot implements Snapshot { * @param _store */ constructor(options: FindOptions, identifier: StableRecordIdentifier, private _store: Store) { - let internalModel = (this._internalModel = _store._internalModelForResource(identifier)); + let internalModel = (this._internalModel = _store._instanceCache._internalModelForResource(identifier)); this.modelName = identifier.type; /** @@ -164,7 +158,7 @@ export default class Snapshot implements Snapshot { } let record = this.record; let attributes = (this.__attributes = Object.create(null)); - let attrs = Object.keys(this._store._attributesDefinitionFor(this.identifier)); + let attrs = Object.keys(this._store.getSchemaDefinitionService().attributesDefinitionFor(this.identifier)); let recordData = this._store._instanceCache.getRecordData(this.identifier); attrs.forEach((keyName) => { if (schemaIsDSModel(this._internalModel.modelClass)) { @@ -315,7 +309,9 @@ export default class Snapshot implements Snapshot { return this._belongsToRelationships[keyName]; } - let relationshipMeta = store._relationshipMetaFor(this.modelName, null, keyName); + let relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ + keyName + ]; assert( `Model '${this.identifier}' has no belongsTo relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'belongsTo' @@ -334,7 +330,7 @@ export default class Snapshot implements Snapshot { importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') ).graphFor; const { identifier } = this; - const relationship = graphFor(this._store._storeWrapper).get(identifier, keyName) as BelongsToRelationship; + 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.`, @@ -348,7 +344,7 @@ export default class Snapshot implements Snapshot { let value = relationship.getData(); let data = value && value.data; - inverseInternalModel = data ? store._internalModelForResource(data) : null; + inverseInternalModel = data ? store._instanceCache._internalModelForResource(data) : null; if (value && value.data !== undefined) { if (inverseInternalModel && !inverseInternalModel.isDeleted()) { @@ -416,7 +412,9 @@ export default class Snapshot implements Snapshot { } let store = this._internalModel.store; - let relationshipMeta = store._relationshipMetaFor(this.modelName, null, keyName); + let relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ + keyName + ]; assert( `Model '${this.identifier}' has no hasMany relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'hasMany' @@ -435,7 +433,7 @@ export default class Snapshot implements Snapshot { importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') ).graphFor; const { identifier } = this; - const relationship = graphFor(this._store._storeWrapper).get(identifier, keyName) as ManyRelationship; + 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 @@ -450,7 +448,7 @@ export default class Snapshot implements Snapshot { if (value.data) { results = []; value.data.forEach((member) => { - let internalModel = store._internalModelForResource(member); + let internalModel = store._instanceCache._internalModelForResource(member); if (!internalModel.isDeleted()) { if (returnModeIsIds) { (results as RecordId[]).push( @@ -492,7 +490,7 @@ export default class Snapshot implements Snapshot { @public */ eachAttribute(callback: (key: string, meta: AttributeSchema) => void, binding?: unknown): void { - let attrDefs = this._store._attributesDefinitionFor(this.identifier); + let attrDefs = this._store.getSchemaDefinitionService().attributesDefinitionFor(this.identifier); Object.keys(attrDefs).forEach((key) => { callback.call(binding, key, attrDefs[key] as AttributeSchema); }); @@ -516,7 +514,7 @@ export default class Snapshot implements Snapshot { @public */ eachRelationship(callback: (key: string, meta: RelationshipSchema) => void, binding?: unknown): void { - let relationshipDefs = this._store._relationshipsDefinitionFor(this.identifier); + let relationshipDefs = this._store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier); Object.keys(relationshipDefs).forEach((key) => { callback.call(binding, key, relationshipDefs[key] as RelationshipSchema); }); diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts deleted file mode 100644 index 0f116151921..00000000000 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ /dev/null @@ -1,1116 +0,0 @@ -import { assert } from '@ember/debug'; -import { _backburner as emberBackburner, cancel, run } from '@ember/runloop'; -import { DEBUG } from '@glimmer/env'; - -import { importSync } from '@embroider/macros'; - -import type { ManyArray } from '@ember-data/model/-private'; -import type { ManyArrayCreateArgs } from '@ember-data/model/-private/system/many-array'; -import type { - BelongsToProxyCreateArgs, - BelongsToProxyMeta, -} from '@ember-data/model/-private/system/promise-belongs-to'; -import type PromiseBelongsTo from '@ember-data/model/-private/system/promise-belongs-to'; -import type { HasManyProxyCreateArgs } from '@ember-data/model/-private/system/promise-many-array'; -import type PromiseManyArray from '@ember-data/model/-private/system/promise-many-array'; -import { HAS_MODEL_PACKAGE, HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; -import type { - BelongsToRelationship, - ManyRelationship, - RecordData as DefaultRecordData, -} from '@ember-data/record-data/-private'; -import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition'; -import type { - DefaultSingleResourceRelationship, - RelationshipRecordData, -} from '@ember-data/record-data/-private/ts-interfaces/relationship-record-data'; - -import type { DSModel } from '../../ts-interfaces/ds-model'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { ChangedAttributesHash, RecordData } from '../../ts-interfaces/record-data'; -import type { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api'; -import type { RelationshipSchema } from '../../ts-interfaces/record-data-schemas'; -import type { RecordInstance } from '../../ts-interfaces/record-instance'; -import type { Dict } from '../../ts-interfaces/utils'; -import type CoreStore from '../core-store'; -import { errorsHashToArray } from '../errors-utils'; -import recordDataFor from '../record-data-for'; -import { BelongsToReference, HasManyReference, RecordReference } from '../references'; -import { internalModelFactoryFor } from '../store/internal-model-factory'; - -type PrivateModelModule = { - ManyArray: { create(args: ManyArrayCreateArgs): ManyArray }; - PromiseBelongsTo: { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; - PromiseManyArray: new (...args: unknown[]) => PromiseManyArray; -}; - -/** - @module @ember-data/store -*/ - -let _ManyArray: PrivateModelModule['ManyArray']; -let _PromiseBelongsTo: PrivateModelModule['PromiseBelongsTo']; -let _PromiseManyArray: PrivateModelModule['PromiseManyArray']; - -let _found = false; -let _getModelPackage: () => boolean; -if (HAS_MODEL_PACKAGE) { - _getModelPackage = function () { - if (!_found) { - let modelPackage = importSync('@ember-data/model/-private') as PrivateModelModule; - ({ - ManyArray: _ManyArray, - PromiseBelongsTo: _PromiseBelongsTo, - PromiseManyArray: _PromiseManyArray, - } = modelPackage); - if (_ManyArray && _PromiseBelongsTo && _PromiseManyArray) { - _found = true; - } - } - return _found; - }; -} - -function isDSModel(record: RecordInstance | null): record is DSModel { - return ( - HAS_MODEL_PACKAGE && - !!record && - 'constructor' in record && - 'isModel' in record.constructor && - record.constructor.isModel === true - ); -} - -export default class InternalModel { - declare _id: string | null; - declare modelName: string; - declare clientId: string; - declare hasRecordData: boolean; - declare _isDestroyed: boolean; - declare isError: boolean; - declare _pendingRecordArrayManagerFlush: boolean; - declare _isDematerializing: boolean; - declare _doNotDestroy: boolean; - declare isDestroying: boolean; - declare _isUpdatingId: boolean; - declare _deletedRecordWasNew: boolean; - - // Not typed yet - declare _scheduledDestroy: any; - declare _modelClass: any; - declare __recordArrays: any; - declare references: any; - declare _recordReference: RecordReference; - declare _manyArrayCache: Dict; - - declare _relationshipPromisesCache: Dict>; - declare _relationshipProxyCache: Dict; - declare error: any; - declare store: CoreStore; - declare identifier: StableRecordIdentifier; - declare hasRecord: boolean; - - constructor(store: CoreStore, identifier: StableRecordIdentifier) { - if (HAS_MODEL_PACKAGE) { - _getModelPackage(); - } - this.store = store; - this.identifier = identifier; - this._id = identifier.id; - this._isUpdatingId = false; - this.modelName = identifier.type; - this.clientId = identifier.lid; - this.hasRecord = false; - - this.hasRecordData = false; - - this._isDestroyed = false; - this._doNotDestroy = false; - this.isError = false; - this._pendingRecordArrayManagerFlush = false; // used by the recordArrayManager - - // During dematerialization we don't want to rematerialize the record. The - // reason this might happen is that dematerialization removes records from - // record arrays, and Ember arrays will always `objectAt(0)` and - // `objectAt(len - 1)` to test whether or not `firstObject` or `lastObject` - // have changed. - this._isDematerializing = false; - this._scheduledDestroy = null; - - this.error = null; - - // caches for lazy getters - this._modelClass = null; - this.__recordArrays = null; - this._recordReference = null; - - this.error = null; - - // other caches - // class fields have [[DEFINE]] semantics which are significantly slower than [[SET]] semantics here - this._manyArrayCache = Object.create(null); - this._relationshipPromisesCache = Object.create(null); - this._relationshipProxyCache = Object.create(null); - this.references = Object.create(null); - } - - get id(): string | null { - return this.identifier.id; - } - set id(value: string | null) { - if (value !== this._id) { - let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value }; - // TODO potentially this needs to handle merged result - this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier); - this.notifyPropertyChange('id'); - } - } - - get modelClass() { - if (this.store.modelFor) { - return this._modelClass || (this._modelClass = this.store.modelFor(this.modelName)); - } - } - - get recordReference(): RecordReference { - if (this._recordReference === null) { - this._recordReference = new RecordReference(this.store, this.identifier); - } - return this._recordReference; - } - - get _recordData(): RecordData { - return this.store._instanceCache.getRecordData(this.identifier); - } - - isHiddenFromRecordArrays() { - // During dematerialization we don't want to rematerialize the record. - // recordWasDeleted can cause other records to rematerialize because it - // removes the internal model from the array and Ember arrays will always - // `objectAt(0)` and `objectAt(len -1)` to check whether `firstObject` or - // `lastObject` have changed. When this happens we don't want those - // models to rematerialize their records. - - // eager checks to avoid instantiating record data if we are empty or loading - if (this.isEmpty) { - return true; - } - - if (this.isLoading) { - return false; - } - - let isRecordFullyDeleted = this._isRecordFullyDeleted(); - return this._isDematerializing || this.hasScheduledDestroy() || this.isDestroyed || isRecordFullyDeleted; - } - - _isRecordFullyDeleted(): boolean { - if (this._recordData.isDeletionCommitted && this._recordData.isDeletionCommitted()) { - return true; - } else if ( - this._recordData.isNew && - this._recordData.isDeleted && - this._recordData.isNew() && - this._recordData.isDeleted() - ) { - return true; - } else { - return false; - } - } - - isDeleted(): boolean { - if (this._recordData.isDeleted) { - return this._recordData.isDeleted(); - } else { - return false; - } - } - - isNew(): boolean { - if (this.hasRecordData && this._recordData.isNew) { - return this._recordData.isNew(); - } else { - return false; - } - } - - get isEmpty(): boolean { - return !this.hasRecordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false; - } - - get isLoading() { - const req = this.store.getRequestStateService(); - const { identifier } = this; - // const fulfilled = req.getLastRequestForRecord(identifier); - - return ( - !this.isLoaded && - // fulfilled === null && - req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query') - ); - } - - get isLoaded() { - // if we are new we must consider ourselves loaded - if (this.isNew()) { - return true; - } - // even if we have a past request, if we are now empty we are not loaded - // typically this is true after an unloadRecord call - - // if we are not empty, not new && we have a fulfilled request then we are loaded - // we should consider allowing for something to be loaded that is simply "not empty". - // which is how RecordState currently handles this case; however, RecordState is buggy - // in that it does not account for unloading. - return !this.isEmpty; - } - - dematerializeRecord() { - this._isDematerializing = true; - - // TODO IGOR add a test that fails when this is missing, something that involves canceling a destroy - // and the destroy not happening, and then later on trying to destroy - this._doNotDestroy = false; - // this has to occur before the internal model is removed - // for legacy compat. - const { identifier } = this; - let hadRecord = this.store._instanceCache.removeRecord(identifier); - - // move to an empty never-loaded state - // ensure any record notifications happen prior to us - // unseting the record but after we've triggered - // destroy - this.store._backburner.join(() => { - this._recordData.unloadRecord(); - }); - - if (hadRecord) { - let keys = Object.keys(this._relationshipProxyCache); - keys.forEach((key) => { - let proxy = this._relationshipProxyCache[key]!; - if (proxy.destroy) { - proxy.destroy(); - } - delete this._relationshipProxyCache[key]; - }); - } - - this.hasRecord = false; // this must occur after relationship removal - this.error = null; - this.store.recordArrayManager.recordDidChange(this.identifier); - } - - deleteRecord() { - run(() => { - const backburner = this.store._backburner; - backburner.run(() => { - if (this._recordData.setIsDeleted) { - this._recordData.setIsDeleted(true); - } - - if (this.isNew()) { - // destroyRecord follows up deleteRecord with save(). This prevents an unecessary save for a new record - this._deletedRecordWasNew = true; - this.unloadRecord(); - } - }); - }); - } - - /* - Unload the record for this internal model. This will cause the record to be - destroyed and freed up for garbage collection. It will also do a check - for cleaning up internal models. - - This check is performed by first computing the set of related internal - models. If all records in this set are unloaded, then the entire set is - destroyed. Otherwise, nothing in the set is destroyed. - - This means that this internal model will be freed up for garbage collection - once all models that refer to it via some relationship are also unloaded. - */ - unloadRecord() { - if (this.isDestroyed) { - return; - } - if (DEBUG) { - const requests = this.store.getRequestStateService().getPendingRequestsForRecord(this.identifier); - if ( - requests.some((req) => { - return req.type === 'mutation'; - }) - ) { - assert('You can only unload a record which is not inFlight. `' + this + '`'); - } - } - this.dematerializeRecord(); - if (this._scheduledDestroy === null) { - this._scheduledDestroy = emberBackburner.schedule('destroy', this, '_checkForOrphanedInternalModels'); - } - } - - hasScheduledDestroy() { - return !!this._scheduledDestroy; - } - - cancelDestroy() { - assert( - `You cannot cancel the destruction of an InternalModel once it has already been destroyed`, - !this.isDestroyed - ); - - this._doNotDestroy = true; - this._isDematerializing = false; - cancel(this._scheduledDestroy); - this._scheduledDestroy = null; - } - - // typically, we prefer to async destroy this lets us batch cleanup work. - // Unfortunately, some scenarios where that is not possible. Such as: - // - // ```js - // const record = store.findRecord(‘record’, 1); - // record.unloadRecord(); - // store.createRecord(‘record’, 1); - // ``` - // - // In those scenarios, we make that model's cleanup work, sync. - // - destroySync() { - if (this._isDematerializing) { - this.cancelDestroy(); - } - this._checkForOrphanedInternalModels(); - if (this.isDestroyed || this.isDestroying) { - return; - } - - // just in-case we are not one of the orphaned, we should still - // still destroy ourselves - this.destroy(); - } - - _checkForOrphanedInternalModels() { - this._isDematerializing = false; - this._scheduledDestroy = null; - if (this.isDestroyed) { - return; - } - } - - _findBelongsTo( - key: string, - resource: DefaultSingleResourceRelationship, - relationshipMeta: RelationshipSchema, - options?: Dict - ): 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.store._findBelongsToByJsonApiResource(resource, this.identifier, relationshipMeta, options).then( - (identifier: StableRecordIdentifier | null) => - handleCompletedRelationshipRequest(this, key, resource._relationship, identifier), - (e) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e) - ); - } - - getBelongsTo(key: string, options?: Dict): PromiseBelongsTo | RecordInstance | null { - let resource = (this._recordData as DefaultRecordData).getBelongsTo(key); - let identifier = - resource && resource.data ? this.store.identifierCache.getOrCreateRecordIdentifier(resource.data) : null; - let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key); - assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta); - - let store = this.store; - let parentInternalModel = this; - let async = relationshipMeta.options.async; - let isAsync = typeof async === 'undefined' ? true : async; - let _belongsToState: BelongsToProxyMeta = { - key, - store, - originatingInternalModel: this, - modelName: relationshipMeta.type, - }; - - if (isAsync) { - if (resource._relationship.state.hasFailedLoadAttempt) { - return this._relationshipProxyCache[key] as PromiseBelongsTo; - } - - let promise = this._findBelongsTo(key, resource, relationshipMeta, options); - - return this._updatePromiseProxyFor('belongsTo', key, { - promise, - content: identifier ? store._instanceCache.getRecord(identifier) : null, - _belongsToState, - }); - } else { - if (identifier === null) { - return null; - } else { - let toReturn = store._instanceCache.getRecord(identifier); - assert( - "You looked up the '" + - key + - "' relationship on a '" + - parentInternalModel.modelName + - "' with id " + - parentInternalModel.id + - ' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`belongsTo({ async: true })`)', - toReturn === null || !store._instanceCache.getInternalModel(identifier).isEmpty - ); - return toReturn; - } - } - } - - getManyArray(key: string, definition?: UpgradedMeta): ManyArray { - assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE); - let manyArray: ManyArray | undefined = this._manyArrayCache[key]; - if (!definition) { - const graphFor = ( - importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') - ).graphFor; - definition = graphFor(this.store).get(this.identifier, key).definition as UpgradedMeta; - } - - if (!manyArray) { - manyArray = _ManyArray.create({ - store: this.store, - type: this.store.modelFor(definition.type), - recordData: this._recordData as RelationshipRecordData, - key, - isPolymorphic: definition.isPolymorphic, - isAsync: definition.isAsync, - _inverseIsAsync: definition.inverseIsAsync, - internalModel: this, - isLoaded: !definition.isAsync, - }); - this._manyArrayCache[key] = manyArray; - } - - return manyArray; - } - - fetchAsyncHasMany( - key: string, - relationship: ManyRelationship, - manyArray: ManyArray, - options?: Dict - ): Promise { - if (HAS_RECORD_DATA_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; - if (loadingPromise) { - return loadingPromise; - } - - const jsonApi = this._recordData.getHasMany(key); - - loadingPromise = this.store._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options).then( - () => handleCompletedRelationshipRequest(this, key, relationship, manyArray), - (e) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e) - ); - this._relationshipPromisesCache[key] = loadingPromise; - return loadingPromise; - } - assert('hasMany only works with the @ember-data/record-data package'); - } - - getHasMany(key: string, options?): PromiseManyArray | ManyArray { - if (HAS_RECORD_DATA_PACKAGE) { - const graphFor = ( - importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') - ).graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; - const { definition, state } = relationship; - let 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); - - return this._updatePromiseProxyFor('hasMany', key, { promise, content: manyArray }); - } else { - assert( - `You looked up the '${key}' relationship on a '${this.modelName}' with id ${this.id} but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async ('hasMany({ async: true })')`, - !anyUnloaded(this.store, relationship) - ); - - return manyArray; - } - } - assert(`hasMany only works with the @ember-data/record-data package`); - } - - _updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray; - _updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo; - _updatePromiseProxyFor( - kind: 'belongsTo', - key: string, - args: { promise: Promise } - ): PromiseBelongsTo; - _updatePromiseProxyFor( - kind: 'hasMany' | 'belongsTo', - key: string, - args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } - ): PromiseBelongsTo | PromiseManyArray { - let promiseProxy = this._relationshipProxyCache[key]; - if (kind === 'hasMany') { - const { promise, content } = args as HasManyProxyCreateArgs; - if (promiseProxy) { - assert(`Expected a PromiseManyArray`, '_update' in promiseProxy); - promiseProxy._update(promise, content); - } else { - promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(promise, content); - } - return promiseProxy; - } - if (promiseProxy) { - const { promise, content } = args as BelongsToProxyCreateArgs; - assert(`Expected a PromiseBelongsTo`, '_belongsToState' in promiseProxy); - - if (content !== undefined) { - promiseProxy.set('content', content); - } - promiseProxy.set('promise', promise); - } else { - // this usage of `any` can be removed when `@types/ember_object` proxy allows `null` for content - this._relationshipProxyCache[key] = promiseProxy = _PromiseBelongsTo.create(args as any); - } - - return promiseProxy; - } - - reloadHasMany(key: string, options) { - if (HAS_RECORD_DATA_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key]; - if (loadingPromise) { - return loadingPromise; - } - const graphFor = ( - importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') - ).graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; - const { definition, state } = relationship; - - state.hasFailedLoadAttempt = false; - state.shouldForceReload = true; - let manyArray = this.getManyArray(key, definition); - let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); - - if (this._relationshipProxyCache[key]) { - return this._updatePromiseProxyFor('hasMany', key, { promise }); - } - - return promise; - } - assert(`hasMany only works with the @ember-data/record-data package`); - } - - reloadBelongsTo(key: string, options?: Dict): Promise { - let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; - if (loadingPromise) { - return loadingPromise; - } - - let resource = (this._recordData as DefaultRecordData).getBelongsTo(key); - // TODO move this to a public api - if (resource._relationship) { - resource._relationship.state.hasFailedLoadAttempt = false; - resource._relationship.state.shouldForceReload = true; - } - let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key); - assert(`Attempted to reload a belongsTo relationship but no definition exists for it`, relationshipMeta); - let promise = this._findBelongsTo(key, resource, relationshipMeta, options); - if (this._relationshipProxyCache[key]) { - return this._updatePromiseProxyFor('belongsTo', key, { promise }); - } - return promise; - } - - destroyFromRecordData() { - if (this._doNotDestroy) { - this._doNotDestroy = false; - return; - } - this.destroy(); - } - - destroy() { - let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); - assert( - 'Cannot destroy an internalModel while its record is materialized', - !record || record.isDestroyed || record.isDestroying - ); - this.isDestroying = true; - if (this._recordReference) { - this._recordReference.destroy(); - } - this._recordReference = null; - let cache = this._manyArrayCache; - Object.keys(cache).forEach((key) => { - cache[key]!.destroy(); - delete cache[key]; - }); - if (this.references) { - cache = this.references; - Object.keys(cache).forEach((key) => { - cache[key]!.destroy(); - delete cache[key]; - }); - } - - internalModelFactoryFor(this.store).remove(this); - this._isDestroyed = true; - } - - setupData(data) { - if (this.isNew()) { - this.store._notificationManager.notify(this.identifier, 'identity'); - } - this._recordData.pushData(data, this.hasRecord); - } - - notifyAttributes(keys: string[]): void { - if (this.hasRecord) { - let manager = this.store._notificationManager; - let { identifier } = this; - - if (!keys || !keys.length) { - manager.notify(identifier, 'attributes'); - } else { - for (let i = 0; i < keys.length; i++) { - manager.notify(identifier, 'attributes', keys[i]); - } - } - } - } - - setDirtyHasMany(key: string, records) { - assertRecordsPassedToHasMany(records); - return this._recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records)); - } - - setDirtyBelongsTo(key: string, value) { - return this._recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value)); - } - - get isDestroyed(): boolean { - return this._isDestroyed; - } - - hasChangedAttributes(): boolean { - if (!this.hasRecordData) { - // no need to calculate changed attributes when calling `findRecord` - return false; - } - return this._recordData.hasChangedAttributes(); - } - - changedAttributes(): ChangedAttributesHash { - if (!this.hasRecordData) { - // no need to calculate changed attributes when calling `findRecord` - return {}; - } - return this._recordData.changedAttributes(); - } - - adapterWillCommit(): void { - this._recordData.willCommit(); - let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); - if (record && isDSModel(record)) { - record.errors.clear(); - } - } - - notifyHasManyChange(key: string) { - if (this.hasRecord) { - let manyArray = this._manyArrayCache[key]; - let hasPromise = !!this._relationshipPromisesCache[key]; - - if (manyArray && hasPromise) { - // do nothing, we will notify the ManyArray directly - // once the fetch has completed. - return; - } - - this.store._notificationManager.notify(this.identifier, 'relationships', key); - } - } - - notifyBelongsToChange(key: string) { - if (this.hasRecord) { - this.store._notificationManager.notify(this.identifier, 'relationships', key); - } - } - - notifyPropertyChange(key: string) { - if (this.hasRecord) { - // TODO this should likely *mostly* be the `attributes` bucket - // but it seems for local mutations we rely on computed updating - // iteself when set. As we design our own thing we may need to change - // that. - this.store._notificationManager.notify(this.identifier, 'property', key); - } - } - - notifyStateChange(key?: string) { - if (this.hasRecord) { - this.store._notificationManager.notify(this.identifier, 'state'); - } - if (!key || key === 'isDeletionCommitted') { - this.store.recordArrayManager.recordDidChange(this.identifier); - } - } - - rollbackAttributes() { - this.store._backburner.join(() => { - let dirtyKeys = this._recordData.rollbackAttributes(); - - let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); - if (record && isDSModel(record)) { - record.errors.clear(); - } - - if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) { - this.notifyAttributes(dirtyKeys); - } - }); - } - - removeFromInverseRelationships() { - if (this.hasRecordData) { - this.store._backburner.join(() => { - this._recordData.removeFromInverseRelationships(); - }); - } - } - - /* - When a find request is triggered on the store, the user can optionally pass in - attributes and relationships to be preloaded. These are meant to behave as if they - came back from the server, except the user obtained them out of band and is informing - the store of their existence. The most common use case is for supporting client side - nested URLs, such as `/posts/1/comments/2` so the user can do - `store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post. - - Preloaded data can be attributes and relationships passed in either as IDs or as actual - models. - */ - preloadData(preload) { - let jsonPayload: JsonApiResource = {}; - //TODO(Igor) consider the polymorphic case - Object.keys(preload).forEach((key) => { - let preloadValue = preload[key]; - let relationshipMeta = this.modelClass.metaForProperty(key); - if (relationshipMeta.isRelationship) { - if (!jsonPayload.relationships) { - jsonPayload.relationships = {}; - } - jsonPayload.relationships[key] = this._preloadRelationship(key, preloadValue); - } else { - if (!jsonPayload.attributes) { - jsonPayload.attributes = {}; - } - jsonPayload.attributes[key] = preloadValue; - } - }); - this._recordData.pushData(jsonPayload); - } - - _preloadRelationship(key, preloadValue) { - let relationshipMeta = this.modelClass.metaForProperty(key); - let modelClass = relationshipMeta.type; - let data; - if (relationshipMeta.kind === 'hasMany') { - assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue)); - data = preloadValue.map((value) => this._convertPreloadRelationshipToJSON(value, modelClass)); - } else { - data = this._convertPreloadRelationshipToJSON(preloadValue, modelClass); - } - return { data }; - } - - _convertPreloadRelationshipToJSON(value, modelClass) { - if (typeof value === 'string' || typeof value === 'number') { - return { type: modelClass, id: value }; - } - let internalModel; - if (value._internalModel) { - internalModel = value._internalModel; - } else { - internalModel = value; - } - // TODO IGOR DAVID assert if no id is present - return { type: internalModel.modelName, id: internalModel.id }; - } - - /* - * calling `store.setRecordId` is necessary to update - * the cache index for this record if we have changed. - * - * However, since the store is not aware of whether the update - * is from us (via user set) or from a push of new data - * it will also call us so that we can notify and update state. - * - * When it does so it calls with `fromCache` so that we can - * short-circuit instead of cycling back. - * - * This differs from the short-circuit in the `_isUpdatingId` - * case in that the the cache can originate the call to setId, - * so on first entry we will still need to do our own update. - */ - setId(id: string | null, fromCache: boolean = false) { - if (this._isUpdatingId === true) { - return; - } - this._isUpdatingId = true; - let didChange = id !== this._id; - this._id = id; - - if (didChange && id !== null) { - if (!fromCache) { - this.store.setRecordId(this.modelName, id, this.clientId); - } - // internal set of ID to get it to RecordData from DS.Model - // if we are within create we may not have a recordData yet. - if (this.hasRecordData && this._recordData.__setId) { - this._recordData.__setId(id); - } - } - - if (didChange && this.hasRecord) { - this.store._notificationManager.notify(this.identifier, 'identity'); - } - this._isUpdatingId = false; - } - - didError() {} - - /* - If the adapter did not return a hash in response to a commit, - merge the changed attributes and relationships into the existing - saved data. - */ - adapterDidCommit(data) { - this._recordData.didCommit(data); - this.store.recordArrayManager.recordDidChange(this.identifier); - } - - hasErrors(): boolean { - // TODO add assertion forcing consuming RecordData's to implement getErrors - if (this._recordData.getErrors) { - return this._recordData.getErrors(this.identifier).length > 0; - } else { - let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' }); - // we can't have errors if we never tried loading - if (!record) { - return false; - } - let errors = (record as DSModel).errors; - return errors.length > 0; - } - } - - // FOR USE DURING COMMIT PROCESS - adapterDidInvalidate(parsedErrors, error?) { - // TODO @runspired this should be handled by RecordState - // and errors should be dirtied but lazily fetch if at - // all possible. We should only notify errors here. - let attribute; - if (error && parsedErrors) { - // TODO add assertion forcing consuming RecordData's to implement getErrors - if (!this._recordData.getErrors) { - let record = this.store._instanceCache.getRecord(this.identifier) as DSModel; - let errors = record.errors; - for (attribute in parsedErrors) { - if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) { - errors.add(attribute, parsedErrors[attribute]); - } - } - } - - let jsonApiErrors: JsonApiValidationError[] = errorsHashToArray(parsedErrors); - if (jsonApiErrors.length === 0) { - jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }]; - } - this._recordData.commitWasRejected(this.identifier, jsonApiErrors); - } else { - this._recordData.commitWasRejected(this.identifier); - } - } - - notifyErrorsChange() { - this.store._notificationManager.notify(this.identifier, 'errors'); - } - - adapterDidError() { - this._recordData.commitWasRejected(); - } - - toString() { - return `<${this.modelName}:${this.id}>`; - } - - referenceFor(kind: string | null, name: string) { - let reference = this.references[name]; - - if (!reference) { - if (!HAS_RECORD_DATA_PACKAGE) { - // 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/record-data`); - } - const graphFor = ( - importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') - ).graphFor; - const relationship = graphFor(this.store._storeWrapper).get(this.identifier, name); - - if (DEBUG && kind) { - let modelName = this.modelName; - let 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 - ); - } - - let relationshipKind = relationship.definition.kind; - let identifierOrInternalModel = this.identifier; - - if (relationshipKind === 'belongsTo') { - reference = new BelongsToReference(this.store, identifierOrInternalModel, relationship, name); - } else if (relationshipKind === 'hasMany') { - reference = new HasManyReference(this.store, identifierOrInternalModel, relationship, name); - } - - this.references[name] = reference; - } - - return reference; - } -} - -function handleCompletedRelationshipRequest( - internalModel: InternalModel, - key: string, - relationship: BelongsToRelationship, - value: StableRecordIdentifier | null -): RecordInstance | null; -function handleCompletedRelationshipRequest( - internalModel: InternalModel, - key: string, - relationship: ManyRelationship, - value: ManyArray -): ManyArray; -function handleCompletedRelationshipRequest( - internalModel: InternalModel, - key: string, - relationship: BelongsToRelationship, - value: null, - error: Error -): never; -function handleCompletedRelationshipRequest( - internalModel: InternalModel, - key: string, - relationship: ManyRelationship, - value: ManyArray, - error: Error -): never; -function handleCompletedRelationshipRequest( - internalModel: InternalModel, - key: string, - relationship: BelongsToRelationship | ManyRelationship, - value: ManyArray | StableRecordIdentifier | null, - error?: Error -): ManyArray | RecordInstance | null { - delete internalModel._relationshipPromisesCache[key]; - relationship.state.shouldForceReload = false; - const isHasMany = relationship.definition.kind === 'hasMany'; - - if (isHasMany) { - // we don't notify the record property here to avoid refetch - // only the many array - (value as ManyArray).notify(); - } - - if (error) { - relationship.state.hasFailedLoadAttempt = true; - let proxy = internalModel._relationshipProxyCache[key]; - // belongsTo relationships are sometimes unloaded - // when a load fails, in this case we need - // to make sure that we aren't proxying - // to destroyed content - // for the sync belongsTo reload case there will be no proxy - // for the async reload case there will be no proxy if the ui - // has never been accessed - if (proxy && !isHasMany) { - if (proxy.content && proxy.content.isDestroying) { - // TODO @types/ember__object incorrectly disallows `null`, we should either - // override or fix upstream - (proxy as PromiseBelongsTo).set('content', null as unknown as undefined); - } - } - - throw error; - } - - if (isHasMany) { - (value as ManyArray).set('isLoaded', true); - } - - relationship.state.hasFailedLoadAttempt = false; - // only set to not stale if no error is thrown - relationship.state.isStale = false; - - return isHasMany || !value - ? (value as ManyArray | null) - : internalModel.store.peekRecord(value as StableRecordIdentifier); -} - -export function assertRecordsPassedToHasMany(records) { - 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 - .map((r) => `${typeof r}`) - .join(', ')}`, - (function () { - return records.every((record) => Object.prototype.hasOwnProperty.call(record, '_internalModel') === true); - })() - ); -} - -export function extractRecordDatasFromRecords(records) { - return records.map(extractRecordDataFromRecord); -} - -export function extractRecordDataFromRecord(recordOrPromiseRecord) { - if (!recordOrPromiseRecord) { - return null; - } - - if (recordOrPromiseRecord.then) { - let content = recordOrPromiseRecord.get && recordOrPromiseRecord.get('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 - ); - return content ? recordDataFor(content) : null; - } - - return recordDataFor(recordOrPromiseRecord); -} - -function anyUnloaded(store: CoreStore, relationship: ManyRelationship) { - let state = relationship.currentState; - const unloaded = state.find((s) => { - let im = store._internalModelForResource(s); - return im._isDematerializing || !im.isLoaded; - }); - - return unloaded || false; -} diff --git a/packages/store/addon/-private/system/record-arrays.ts b/packages/store/addon/-private/system/record-arrays.ts deleted file mode 100644 index dfe729d756d..00000000000 --- a/packages/store/addon/-private/system/record-arrays.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - @module @ember-data/store -*/ - -import AdapterPopulatedRecordArray from './record-arrays/adapter-populated-record-array'; -import RecordArray from './record-arrays/record-array'; - -export { RecordArray, AdapterPopulatedRecordArray }; diff --git a/packages/store/addon/-private/system/record-data-for.ts b/packages/store/addon/-private/system/record-data-for.ts deleted file mode 100644 index 32a05facff4..00000000000 --- a/packages/store/addon/-private/system/record-data-for.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; - -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type { RecordData } from '../ts-interfaces/record-data'; -import type { RecordInstance } from '../ts-interfaces/record-instance'; -import WeakCache from './weak-cache'; - -/* - * Returns the RecordData instance associated with a given - * Model or InternalModel. - * - * Intentionally "loose" to allow anything with an _internalModel - * property until InternalModel is eliminated. - * - * Intentionally not typed to `InternalModel` due to circular dependency - * which that creates. - * - * Overtime, this should shift to a "weakmap" based lookup in the - * "Ember.getOwner(obj)" style. - */ -interface InternalModel { - _recordData: RecordData; -} - -type DSModelOrSnapshot = { _internalModel: InternalModel }; -type Reference = { internalModel: InternalModel }; - -type Instance = StableRecordIdentifier | InternalModel | RecordData | DSModelOrSnapshot | Reference; - -const RecordDataForIdentifierCache = new WeakCache( - DEBUG ? 'recordData' : '' -); - -export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: RecordData): void { - assert( - `Illegal set of identifier`, - !RecordDataForIdentifierCache.has(identifier) || RecordDataForIdentifierCache.get(identifier) === recordData - ); - RecordDataForIdentifierCache.set(identifier, recordData); -} - -export function removeRecordDataFor(identifier: StableRecordIdentifier): void { - RecordDataForIdentifierCache.delete(identifier); -} - -export default function recordDataFor(instance: StableRecordIdentifier): RecordData | null; -export default function recordDataFor(instance: Instance): RecordData; -export default function recordDataFor(instance: RecordInstance): RecordData; -export default function recordDataFor(instance: object): null; -export default function recordDataFor(instance: Instance | object): RecordData | null { - if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { - return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as RecordData; - } - - let internalModel = - (instance as DSModelOrSnapshot)._internalModel || (instance as Reference).internalModel || instance; - - assert(`Expected to no longer need this`, !internalModel._recordData); - - return null; -} diff --git a/packages/store/addon/-private/system/references.js b/packages/store/addon/-private/system/references.js deleted file mode 100644 index 896d5f47cef..00000000000 --- a/packages/store/addon/-private/system/references.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - @module @ember-data/store -*/ - -import BelongsToReference from './references/belongs-to'; -import HasManyReference from './references/has-many'; -import RecordReference from './references/record'; - -export { RecordReference, BelongsToReference, HasManyReference }; diff --git a/packages/store/addon/-private/system/references/reference.ts b/packages/store/addon/-private/system/references/reference.ts deleted file mode 100644 index 85a989602a0..00000000000 --- a/packages/store/addon/-private/system/references/reference.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { Object as JSONObject, Value as JSONValue } from 'json-typescript'; - -import type { LinkObject, PaginationLinks } from '../../ts-interfaces/ember-data-json-api'; -import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { JsonApiRelationship } from '../../ts-interfaces/record-data-json-api'; -import type { Dict } from '../../ts-interfaces/utils'; -import type CoreStore from '../core-store'; -/** - @module @ember-data/store -*/ - -interface ResourceIdentifier { - links?: { - related?: string; - }; - meta?: JSONObject; -} - -function isResourceIdentiferWithRelatedLinks( - value: any -): value is ResourceIdentifier & { links: { related: string | LinkObject | null } } { - return value && value.links && value.links.related; -} - -/** - This is the baseClass for the different References - like RecordReference/HasManyReference/BelongsToReference - - @class Reference - @public - */ -interface Reference { - links(): PaginationLinks | null; -} -abstract class Reference { - #identifier: StableRecordIdentifier; - - constructor(public store: CoreStore, identifier: StableRecordIdentifier) { - this.#identifier = identifier; - } - - get recordData() { - return this.store.recordDataFor(this.#identifier, false); - } - - public _resource(): ResourceIdentifier | JsonApiRelationship | void {} - - /** - This returns a string that represents how the reference will be - looked up when it is loaded. If the relationship has a link it will - use the "link" otherwise it defaults to "id". - - Example - - ```app/models/post.js - import Model, { hasMany } from '@ember-data/model'; - - export default Model.extend({ - comments: hasMany({ async: true }) - }); - ``` - - ```javascript - let post = store.push({ - data: { - type: 'post', - id: 1, - relationships: { - comments: { - data: [{ type: 'comment', id: 1 }] - } - } - } - }); - - let commentsRef = post.hasMany('comments'); - - // get the identifier of the reference - if (commentsRef.remoteType() === "ids") { - let ids = commentsRef.ids(); - } else if (commentsRef.remoteType() === "link") { - let link = commentsRef.link(); - } - ``` - - @method remoteType - @public - @return {String} The name of the remote type. This should either be "link" or "ids" - */ - remoteType(): 'link' | 'id' | 'ids' | 'identity' { - let value = this._resource(); - if (isResourceIdentiferWithRelatedLinks(value)) { - return 'link'; - } - return 'id'; - } - - /** - The link Ember Data will use to fetch or reload this belongs-to - relationship. By default it uses only the "related" resource linkage. - - Example - - ```javascript - // models/blog.js - import Model, { belongsTo } from '@ember-data/model'; - export default Model.extend({ - user: belongsTo({ async: true }) - }); - - let blog = store.push({ - data: { - type: 'blog', - id: 1, - relationships: { - user: { - links: { - related: '/articles/1/author' - } - } - } - } - }); - let userRef = blog.belongsTo('user'); - - // get the identifier of the reference - if (userRef.remoteType() === "link") { - let link = userRef.link(); - } - ``` - - @method link - @public - @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship. - */ - link(): string | null { - let link; - let resource = this._resource(); - - if (isResourceIdentiferWithRelatedLinks(resource)) { - if (resource.links) { - link = resource.links.related; - link = !link || typeof link === 'string' ? link : link.href; - } - } - return link || null; - } - - links(): PaginationLinks | null { - let resource = this._resource(); - - return resource && resource.links ? resource.links : null; - } - - /** - The meta data for the belongs-to relationship. - - Example - - ```javascript - // models/blog.js - import Model, { belongsTo } from '@ember-data/model'; - export default Model.extend({ - user: belongsTo({ async: true }) - }); - - let blog = store.push({ - data: { - type: 'blog', - id: 1, - relationships: { - user: { - links: { - related: { - href: '/articles/1/author' - }, - }, - meta: { - lastUpdated: 1458014400000 - } - } - } - } - }); - - let userRef = blog.belongsTo('user'); - - userRef.meta() // { lastUpdated: 1458014400000 } - ``` - - @method meta - @public - @return {Object} The meta information for the belongs-to relationship. - */ - meta() { - let meta: Dict | null = null; - let resource = this._resource(); - if (resource && resource.meta && typeof resource.meta === 'object') { - meta = resource.meta; - } - return meta; - } -} - -export default Reference; diff --git a/packages/store/addon/-private/utils/construct-resource.ts b/packages/store/addon/-private/utils/construct-resource.ts index 55d4b995bc6..079a24d1602 100644 --- a/packages/store/addon/-private/utils/construct-resource.ts +++ b/packages/store/addon/-private/utils/construct-resource.ts @@ -1,8 +1,12 @@ import { assert } from '@ember/debug'; -import isStableIdentifier from '../identifiers/is-stable-identifier'; -import coerceId from '../system/coerce-id'; -import type { ExistingResourceIdentifierObject, ResourceIdentifierObject } from '../ts-interfaces/ember-data-json-api'; +import type { + ExistingResourceIdentifierObject, + ResourceIdentifierObject, +} from '@ember-data/types/q/ember-data-json-api'; + +import coerceId from '../coerce-id'; +import { isStableIdentifier } from '../identifier-cache'; import isNonEmptyString from './is-non-empty-string'; function constructResource(type: ResourceIdentifierObject): ResourceIdentifierObject; diff --git a/packages/store/addon/-private/utils/promise-record.ts b/packages/store/addon/-private/utils/promise-record.ts index 9ee2d9f635d..03c182fd1ca 100644 --- a/packages/store/addon/-private/utils/promise-record.ts +++ b/packages/store/addon/-private/utils/promise-record.ts @@ -1,23 +1,12 @@ -import CoreStore from '../system/core-store'; -import type { PromiseObject } from '../system/promise-proxies'; -import { promiseObject } from '../system/promise-proxies'; -import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; -import type { RecordInstance } from '../ts-interfaces/record-instance'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; -/** - @module @ember-data/store -*/ +import type Store from '../core-store'; +import type { PromiseObject } from '../promise-proxies'; +import { promiseObject } from '../promise-proxies'; -/** - * Get the materialized model from the internalModel/promise - * that returns an internal model and return it in a promiseObject. - * - * Useful for returning from find methods - * - * @internal - */ export default function promiseRecord( - store: CoreStore, + store: Store, promise: Promise, label?: string ): PromiseObject { diff --git a/packages/store/addon/-private/system/weak-cache.ts b/packages/store/addon/-private/weak-cache.ts similarity index 98% rename from packages/store/addon/-private/system/weak-cache.ts rename to packages/store/addon/-private/weak-cache.ts index 9463f30572d..d1dbbfb26dc 100644 --- a/packages/store/addon/-private/system/weak-cache.ts +++ b/packages/store/addon/-private/weak-cache.ts @@ -1,6 +1,6 @@ import { DEBUG } from '@glimmer/env'; -import { DEBUG_IDENTIFIER_BUCKET } from '../ts-interfaces/identifier'; +import { DEBUG_IDENTIFIER_BUCKET } from './identifer-debug-consts'; /* DEBUG only fields. Keeping this in a separate interface diff --git a/packages/store/types/@ember/object/compat.d.ts b/packages/store/types/@ember/object/compat.d.ts deleted file mode 100644 index ea7129103f5..00000000000 --- a/packages/store/types/@ember/object/compat.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export function dependentKeyCompat(desc: PropertyDescriptor): void; -export function dependentKeyCompat(target: any, key: string, desc: PropertyDescriptor): void; diff --git a/packages/store/types/@ember/utils/index.d.ts b/packages/store/types/@ember/utils/index.d.ts deleted file mode 100644 index 17c9f4f5c7d..00000000000 --- a/packages/store/types/@ember/utils/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36809 -export function typeOf(v: any): 'object' | 'undefined'; - -export { isEqual, isPresent, isNone } from '@ember/utils'; diff --git a/packages/store/types/@ember/version.d.ts b/packages/store/types/@ember/version.d.ts deleted file mode 100644 index 870e5c0acbb..00000000000 --- a/packages/store/types/@ember/version.d.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION: string = ''; diff --git a/packages/store/types/@glimmer/tracking.d.ts b/packages/store/types/@glimmer/tracking.d.ts deleted file mode 100644 index 68d2e1b3749..00000000000 --- a/packages/store/types/@glimmer/tracking.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { tracked } from '@glimmer/tracking'; - -export function cached(target: any, key: string, desc: PropertyDescriptor): void; diff --git a/packages/store/types/ember/index.d.ts b/packages/store/types/ember/index.d.ts deleted file mode 100644 index bc7e03a32b9..00000000000 --- a/packages/store/types/ember/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function run(callback: Function); -export const ENV: { - DS_WARN_ON_UNKNOWN_KEYS?: boolean; -}; -export function meta(obj: Object): any; diff --git a/packages/store/types/fastboot/index.d.ts b/packages/store/types/fastboot/index.d.ts deleted file mode 100644 index 818a8a945a4..00000000000 --- a/packages/store/types/fastboot/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface FastBoot { - require(moduleName: string): any; -} -const FastBoot: undefined | FastBoot; diff --git a/packages/store/types/require/index.d.ts b/packages/store/types/require/index.d.ts deleted file mode 100644 index c13234c4069..00000000000 --- a/packages/store/types/require/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function (moduleName: string): any; - -export function has(moduleName: string): boolean; 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 index b4e4cab645e..fea23e3cfec 100644 --- 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 @@ -5,7 +5,7 @@ import { DEBUG } from '@glimmer/env'; import QUnit from 'qunit'; import semver from 'semver'; -import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; +import type { Dict } from '@ember-data/types/q/utils'; import { checkMatcher } from './check-matcher'; import isThenable from './utils/is-thenable'; diff --git a/root-tsconfig.json b/root-tsconfig.json index 8893f173a8c..4374115c57b 100644 --- a/root-tsconfig.json +++ b/root-tsconfig.json @@ -29,6 +29,8 @@ "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/addon"], "@ember-data/store/*": ["packages/store/addon/*"], "@ember-data/debug": ["packages/debug/addon"], @@ -52,14 +54,16 @@ "@ember-data/unpublished-test-infra/test-support/*": ["packages/unpublished-test-infra/addon-test-support/*"], "fastboot-test-app/tests/*": ["tests/*"], "fastboot-test-app/*": ["app/*"], - "*": ["packages/store/types/*", "packages/record-data/types/*", "packages/fastboot-test-app/types/*"] + "*": ["@types/*", "packages/fastboot-test-app/types/*"] } }, "include": [ + "@types/**/*", + "ember-data-types/**/*", "packages/**/app/**/*", "packages/**/addon/**/*", "packages/**/tests/**/*", - "packages/**/types/**/*", + "packages/fastboot-test-app/types/**/*", "packages/**/test-support/**/*", "packages/**/addon-test-support/**/*" ], diff --git a/tsconfig.json b/tsconfig.json index 9cb12d4b7bd..1d210d578df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,16 +18,6 @@ "packages/unpublished-fastboot-test-app/app/config/environment.d.ts", "packages/unpublished-fastboot-test-app/app/app.ts", "packages/unpublished-fastboot-test-app/app/adapters/application.ts", - "packages/store/types/require/index.d.ts", - "packages/store/types/ember-data-qunit-asserts/index.d.ts", - "packages/store/types/fastboot/index.d.ts", - "packages/store/types/ember/index.d.ts", - "packages/store/types/@glimmer/tracking.d.ts", - "packages/store/types/@ember/utils/index.d.ts", - "packages/store/types/@ember/runloop/index.d.ts", - "packages/store/types/@ember/runloop/-private/backburner.d.ts", - "packages/store/types/@ember/object/compat.d.ts", - "packages/store/types/@ember/debug/index.d.ts", "packages/store/tests/dummy/app/routes/application/route.ts", "packages/store/tests/dummy/app/router.ts", "packages/store/tests/dummy/app/resolver.ts", @@ -37,53 +27,45 @@ "packages/store/addon/-private/utils/promise-record.ts", "packages/store/addon/-private/utils/is-non-empty-string.ts", "packages/store/addon/-private/utils/construct-resource.ts", - "packages/store/addon/-private/ts-interfaces/utils.ts", - "packages/store/addon/-private/ts-interfaces/schema-definition-service.ts", - "packages/store/addon/-private/ts-interfaces/record-instance.ts", - "packages/store/addon/-private/ts-interfaces/record-data.ts", - "packages/store/addon/-private/ts-interfaces/record-data-store-wrapper.ts", - "packages/store/addon/-private/ts-interfaces/record-data-schemas.ts", - "packages/store/addon/-private/ts-interfaces/record-data-record-wrapper.ts", - "packages/store/addon/-private/ts-interfaces/record-data-json-api.ts", - "packages/store/addon/-private/ts-interfaces/promise-proxies.ts", - "packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts", - "packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts", - "packages/store/addon/-private/ts-interfaces/identifier.ts", - "packages/store/addon/-private/ts-interfaces/fetch-manager.ts", - "packages/store/addon/-private/ts-interfaces/ember-data-json-api.ts", - "packages/store/addon/-private/ts-interfaces/ds-model.ts", - "packages/store/addon/-private/system/store/record-data-store-wrapper.ts", - "packages/store/addon/-private/system/store/internal-model-factory.ts", - "packages/store/addon/-private/system/snapshot.ts", - "packages/store/addon/-private/system/snapshot-record-array.ts", - "packages/store/addon/-private/system/schema-definition-service.ts", - "packages/store/addon/-private/system/request-cache.ts", - "packages/store/addon/-private/system/references/belongs-to.ts", - "packages/store/addon/-private/system/references/has-many.ts", - "packages/store/addon/-private/system/references/reference.ts", - "packages/store/addon/-private/system/references/record.ts", - "packages/store/addon/-private/system/record-notification-manager.ts", - "packages/store/addon/-private/system/record-data-for.ts", - "packages/store/addon/-private/system/record-arrays.ts", - "packages/store/addon/-private/system/normalize-model-name.ts", - "packages/store/addon/-private/system/model/shim-model-class.ts", - "packages/store/addon/-private/system/model/internal-model.ts", - "packages/store/addon/-private/system/internal-model-map.ts", - "packages/store/addon/-private/system/identity-map.ts", - "packages/store/addon/-private/system/fetch-manager.ts", - "packages/store/addon/-private/system/core-store.ts", - "packages/store/addon/-private/system/coerce-id.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.ts", + "ember-data-types/q/record-data-store-wrapper.ts", + "ember-data-types/q/record-data-schemas.ts", + "ember-data-types/q/record-data-record-wrapper.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/addon/-private/record-data-store-wrapper.ts", + "packages/store/addon/-private/internal-model-factory.ts", + "packages/store/addon/-private/snapshot.ts", + "packages/store/addon/-private/snapshot-record-array.ts", + "packages/store/addon/-private/schema-definition-service.ts", + "packages/store/addon/-private/request-cache.ts", + "packages/store/addon/-private/record-notification-manager.ts", + "packages/store/addon/-private/record-data-for.ts", + "packages/store/addon/-private/normalize-model-name.ts", + "packages/store/addon/-private/model/shim-model-class.ts", + "packages/store/addon/-private/model/internal-model.ts", + "packages/store/addon/-private/internal-model-map.ts", + "packages/store/addon/-private/identity-map.ts", + "packages/store/addon/-private/fetch-manager.ts", + "packages/store/addon/-private/core-store.ts", + "packages/store/addon/-private/coerce-id.ts", "packages/store/addon/-private/index.ts", - "packages/store/addon/-private/identifiers/utils/uuid-v4.ts", - "packages/store/addon/-private/identifiers/is-stable-identifier.ts", - "packages/store/addon/-private/identifiers/cache.ts", + "packages/store/addon/-private/identifier-cache.ts", "packages/serializer/tests/dummy/app/routes/application/route.ts", "packages/serializer/tests/dummy/app/router.ts", "packages/serializer/tests/dummy/app/resolver.ts", "packages/serializer/tests/dummy/app/config/environment.d.ts", "packages/serializer/tests/dummy/app/app.ts", "packages/serializer/addon/index.ts", - "packages/record-data/types/@ember/polyfills/index.d.ts", "packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts", "packages/record-data/tests/integration/graph/graph-test.ts", "packages/record-data/tests/integration/graph/operations-test.ts", @@ -97,7 +79,7 @@ "packages/record-data/tests/dummy/app/resolver.ts", "packages/record-data/tests/dummy/app/config/environment.d.ts", "packages/record-data/tests/dummy/app/app.ts", - "packages/record-data/addon/-private/ts-interfaces/relationship-record-data.ts", + "ember-data-types/q/relationship-record-data.ts", "packages/record-data/addon/-private/relationships/state/implicit.ts", "packages/record-data/addon/-private/relationships/state/has-many.ts", "packages/record-data/addon/-private/relationships/state/belongs-to.ts", @@ -126,9 +108,9 @@ "packages/model/tests/dummy/app/app.ts", "packages/model/addon/index.ts", "packages/model/addon/-private/util.ts", - "packages/model/addon/-private/system/relationships/relationship-meta.ts", - "packages/model/addon/-private/system/promise-many-array.ts", - "packages/model/addon/-private/system/model-for-mixin.ts", + "packages/model/addon/-private/relationship-meta.ts", + "packages/model/addon/-private/promise-many-array.ts", + "packages/model/addon/-private/model-for-mixin.ts", "packages/model/addon/-private/record-state.ts", "packages/model/addon/-private/notify-changes.ts", "packages/model/addon/-private/index.ts", @@ -139,7 +121,6 @@ "packages/debug/tests/dummy/app/app.ts", "packages/canary-features/addon/index.ts", "packages/canary-features/addon/default-features.ts", - "packages/adapter/types/require/index.d.ts", "packages/adapter/tests/dummy/app/routes/application/route.ts", "packages/adapter/tests/dummy/app/router.ts", "packages/adapter/tests/dummy/app/resolver.ts",